I followed this wonderful tutorial Made with love ES laravel tutorial to implement ES in my Laravel Ecommerce app i am building. I got it to work but i wanted to tweak it a little because as of now, the results only work when i my query term matches exactly an entire word i have in one of my products.
i have a health nut bar called bear naked almond bar, but when i type "bea" it doesn't match anything, but when i type "bear" then it works.
This is my search query as of now.
$model = new ProductsListing;
$items = $this->elasticsearch->search([
'index' => $model->getSearchIndex(),
'type' => $model->getSearchType(),
'body' => [
'query' => [
'multi_match' => [
'fields' => ['brand', 'name^5', 'description'],
'query' => $query,
],
],
],
]);
I would need some help to adjust the query so that i get results as i type.
I am not familiar with PHP and laravel, but the reason why it's not giving result on bea b/c you are using match query which applies the same analyzer which is used at index time, and your text bear naked almond bar creates bear, naked, almond and bar tokens and bea doesn't match any token.
You can change your query below(not sure about the correct syntax for the prefix in laravel).
$model = new ProductsListing;
$items = $this->elasticsearch->search([
'index' => $model->getSearchIndex(),
'type' => $model->getSearchType(),
'body' => [
'query' => [
'prefix' => [. --> changed to prefix query.
'fields' => ['brand', 'name^5', 'description'],
'query' => $query,
],
],
],
]);
I am assuming that you are using the text field with default standard analyzer and using the analyze API, you can check the tokens generated for your text.
POST your-index/_analyze
{
"text": "bear naked almond bar",
"analyzer" : "standard"
}
{
"tokens": [
{
"token": "bear",
"start_offset": 0,
"end_offset": 4,
"type": "<ALPHANUM>",
"position": 0
},
{
"token": "naked",
"start_offset": 5,
"end_offset": 10,
"type": "<ALPHANUM>",
"position": 1
},
{
"token": "almond",
"start_offset": 11,
"end_offset": 17,
"type": "<ALPHANUM>",
"position": 2
},
{
"token": "bar",
"start_offset": 18,
"end_offset": 21,
"type": "<ALPHANUM>",
"position": 3
}
]
}
Try match_phrase_prefix with must query inside bool query instead of multi_match ... you will start getting result as you type...
e.g
$query = '{
"size": 10, //size of return array
"query": {
"bool": {
"must": [
{
"match_phrase_prefix": {
"tag":"' . $filter_name . '"
}
}
],
}
}
}';
//Now pass this $query variable to your ES request
there are two methods 1- prefix , 2- match_phrase_prefix
Please see documentation for prefix and match_phrase_prefix
I was able to get help here: github
and this was the perfect solution. Thank you Enricco Zimuel.
$elasticsearch = ClientBuilder::create()
->setHosts(config('services.search.hosts'))
->build();
$model = new ProductsListing;
$items = $elasticsearch->search([
'index' => $model->getSearchIndex(),
'type' => $model->getSearchType(),
'body' => [
//this helped a lot
// https://stackoverflow.com/questions/60716639/how-to-implement-elastic-search-in-laravel-for-auto-complete
//using query_string
'query' => [
'query_string' => [
// 'fields' => ['brand', 'name^5', 'description'], //notice the weight operator ^5
'fields' => ['name'],
'query' => $query.'*',
],
],
],
]);
Related
I have a query that looks like this where I fetch data for various businesses in a particular location and I need to be able to tell that each business has (or does not have) a female employee.
$business = Business::where('location', $location)
->with(['staff'])
->get();
return MiniResource::collection($business);
My Mini Resource looks like this:
return [
'name' => $this->name,
'location' => $this->location,
'staff' => PersonResource::collection($this->whenLoaded('staff')),
];
This is what a sample response looks like:
{
"id": 1,
"name": "XYZ Business"
"location": "London",
"staff": [
{
"name": "Abigail",
"gender": "f",
"image": "https://s3.amazonaws.com/xxxx/people/xxxx.png",
"role": "Project Manager",
},
{
"name": "Ben",
"gender": "m",
"image": "https://s3.amazonaws.com/xxxx/people/xxxx.png",
"role": "Chef",
},
]
}
I really don't need the staff array, I just want to check that a female exists in the relation and then return something similar to this:
{
"id": 1,
"name": "XYZ Business"
"country": "USA",
"has_female_employee": true;
}
Is there an eloquent way to achieve this ?
NB: In my original code I have more relations that I query but I had to limit this post to be within the scope of my problem.
If you are only looking for male or female staff members, you can achieve it like so:
$someQuery->whereHas('staff', function ($query) {
$query->where('gender', 'f');
})
If you want both genders, I wouldn't go through the hassle of achieving this in the query, but recommend reducing your results collection in your MiniResource:
return [
'name' => $this->name,
'location' => $this->location,
'has_female_employee' => $this->whenLoaded('staff')->reduce(
function ($hasFemale, $employee) {
$hasFemale = $hasFemale || ($employee->gender === 'f');
return $hasFemale;
}, false),
];
Even better would be to create it as a method on your MiniResource for readability.
Change your code like below and see
$business = Business::where('location', $location)
->with(['staff'])
->where('gender', 'f')
->get();
return [
'name' => $this->name,
'location' => $this->location,
'has_female_employee' => empty($this->whenLoaded('staff')) ? false : true,
];
I have a problem with the Laravel Query Builder. When I try to bind a variable, which would include some sort of sql code, into my binding parameter, it returns no results. If I run enableQueryLog() I can see that the query and the binding is correct. So the code provides a perfectly fine query but yet it does not perform accordingly.
I've already tried printing out all the variables that matter. I enabled a query log to see if everything is set correctly, which it is. When I put in the variable in my whereRaw() just as it is without binding, it works fine. Just not with the binding.
This is the code I run:
public function getCarbrands() {
$name = 'name != Ford';
try {
$brands = DB::table('ni_carbrands')
->whereRaw(':name',['name'=>$name])
->select('id','name')
->get();
echo json_encode( array('info' => 1 ,'status' => 'Successfully found car brands', 'details' => $brands));
} catch(Exception $e){
echo json_encode( array('info' => 0 ,'status' => 'Error finding car brands', 'e' => $e));
}
}
I know that this use of the binding feature is unnecessary, it is merely a test for some other functions I wanna build.
This is what my Query Log returns:
array:1 [▼
0 => array:3 [▼
"query" => "select `id`, `name` from `ni_carbrands` where :name"
"bindings" => array:1 [▼
0 => "name != Ford"
]
"time" => 190.25
]
]
The components of the query all seem correct, but yet it seems to have some trouble producing it.
The expected results would be something like this:
{
"info": 1,
"status": "Successfully found car brands",
"details": [
{
"id": 1,
"name": "Toyota"
},
{
"id": 2,
"name": "Fiat"
},
{
"id": 3,
"name": "Iveco"
},
{
"id": 4,
"name": "Citroën"
},
{
"id": 5,
"name": "Opel"
},
{
"id": 6,
"name": "Mercedes"
},
{
"id": 8,
"name": "Volkswagen"
},
{
"id": 9,
"name": "Renault"
},
{
"id": 10,
"name": "MAN"
},
{
"id": 11,
"name": "Nissan"
},
{
"id": 12,
"name": "Hyundai"
},
{
"id": 13,
"name": "Peugeot"
}
]
}
But the actual result is:
{"info":1,"status":"Successfully found car brands","details":[]}
I'd greatly appreciate some help.
It seems like you cant bind a string containing an operator.
Have a look into this one Can I bind a parameter to a PDO statement as a comparison operator?
And this one binding not null in pdo
Bad raw condition:
whereRaw(':name',['name'=>$name])
Just like this:
$brands = DB::table('ni_carbrands')
->select('id','name')
->whereRaw($name)
->get();
you can this use this kind of also
$brands = DB::table('ni_carbrands')
->select('id','name')
->where('name', '!=', 'Ford')
->get();
otherwise you can set where condition in dynamic field
like
->where($name, '!=', 'Ford')
$name = 'name != Ford';
$brands = DB::table('ni_carbrands')
->whereRaw(':name',['name'=>$name])
->select('id','name')
->get();
this is equivalent query
select from ni_carbrands where 'name != Ford'
of course it doesn't work, because you have so
select from ni_carbrands where name != Ford
problem in quotes
I do have have products and some of them are reduced in price for a specific date range.
(simplified) example products:
{
"id": 1,
"price": 2.0,
"specialPrice": {
"fromDate": null,
"tillDate": null,
"value": 0,
},
},
{
"id": 2,
"price": 4.0,
"specialPrice": {
"fromDate": 1540332000,
"tillDate": 1571781600,
"value": 2.5,
},
},
{
"id": 3,
"price": 3.0,
"specialPrice": {
"fromDate": null,
"tillDate": null,
"value": 0,
},
}
Filtering by price was no problem. That I could do with a simple bool query.
But I could not yet find a good example for ElasticSearch scripts that could point me in the right direction, even though it should be quite simple, given you know the syntax.
My pseudocode: price = ('now' between specialPrice.fromDate and specialPrice.tillDate) ? specialPrice.value : price
Is there a way to translate this into something that would work in an ElasticSearch sorting?
To clarify further: By default, all products are already sorted by several conditions. The user can also search for any terms and filter the results while also being able to select multiple sorting parameters. Items can for example be sorted by tags and then by price, it's all very dynamic and it does still sort those results by some other properties (including the _score) afterwards.
So just changing the _score would be bad, since that is already calculated in a complex matter to show the best results for the given search terms.
Here is my current script, which does fail at the first params.currentDate:
"sort": {
"_script": {
"type": "number",
"script": {
"source": "if(doc['specialPrice.tillDate'] > params.currentDate) {params.currentPrice = doc['specialPrice.value']} return params.currentPrice",
"params": {
"currentDate": "now",
"currentPrice": "doc['price']"
}
}
}
How it does work now:
One problem was the nesting of some of the properties.
So one of my steps was to duplicate their content to new fields for the product (which I'm not that happy about, but whatever).
So in my mapping, I created new properties for products (specialFrom, specialTill, specialValue) and gave the corresponding fields in my specialPrice "copy_to" properties with the new property names.
The part is in php array syntax, since I'm using ruflin/elastica:
'specialPrice' => [
'type' => 'nested',
'properties' => [
'fromDate' => [
'type' => 'date',
'format' => 'epoch_second',
'copy_to' => 'specialFrom',
],
'tillDate' => [
'type' => 'date',
'format' => 'epoch_second',
'copy_to' => 'specialTill',
],
'value' => [
'type' => 'float',
'copy_to' => 'specialValue',
],
],
],
'specialFrom' => [
'type' => 'date',
'format' => 'epoch_second',
],
'specialTill' => [
'type' => 'date',
'format' => 'epoch_second',
],
'specialValue' => [
'type' => 'float',
],
Now my sorting sorting script does look like this (in my testing client, still working on implementing it within elastica):
"sort": {
"_script": {
"type": "number",
"script": {
"lang": "painless",
"source": "params.param = ((doc['specialTill'].value - new Date().getTime()) > 0 && (new Date().getTime() - doc['specialFrom'].value) > 0) ? doc['specialValue'].value : doc['price'].value; return params.param;",
"params": {
"param": 0.0
}
}
}
}
I'm not 100% happy with this because I have redundant data and scripts (calling new Date().getTime() twice in the script), but it does work and that is the most important thing for now :)
I've updated the below query post your clarifications. Let me know if that works!
POST dateindex/_search
{
"query":{
"match_all":{ // you can ignore this, I used this to test at my end
}
},
"sort":{
"_script":{
"type":"number",
"script":{
"lang":"painless",
"inline":" params.param = ((doc['specialPrice.tillDate'].value - new Date().getTime()) > 0) ? doc['specialPrice.value'].value : doc['price'].value; return params.param;",
"params":{
"param":0.0
}
},
"order":"asc"
}
}
}
You can try using source instead of inline in the above query as I've been testing on ES5.X version on my machine.
Hope it helps!
I am using the PHP client for elasticsearch (5.2.0) and fail to get the inner_hits results , this is my PHP query (which does not return the inner_hits)
$params = [
'index' => 'caption_index',
'type' => 'caption',
'body' => [
'query' => [
'nested' => [
'path' => 'lines',
'query' => [
'bool' => [
'must' => [
['match' => ['lines.content' => 'Totally different text' ]]
]
]
],
'inner_hits' => [ ]
]
]
],
'client' => [ 'ignore' => 404 ]
];
$results = $client->search($params);
Simultaneously I am running the same requests on Kibana and I do get the answers correctly
GET /caption_index/caption/_search
{
"query": {
"nested" : {
"path" : "lines" ,
"query": {
"bool" : {
"must": [
{
"match" :
{ "lines.content" : "Totally different text" }
}
]
}
},
"inner_hits" : {}
}
}
}
Any idea what is the difference and why the PHP won't show the results?
I can attach the current results but it seems like an overkill in this case - trust me - the inner hits is not there
I was having the same issue with the ES PHP API, got it working by including a parameter in the inner_hits array.
For example:
'inner_hits' => ['name' => 'any-name']
You can find which parameters are allowed here.
I can't find results when filtering by category. Removing the category filter works.
After much experimentation, this is my query:
"query": {
"filtered": {
"query": {
"multi_match": {
"query": "*",
"zero_terms_query": "all",
"operator": "and",
"fields": [
"individual_name.name^1.3",
"organisation_name.name^1.8",
"profile",
"accreditations"
]
}
},
"filter": {
"bool": {
"must": [{
"term": { "categories" : "9" }
]}
}
}
}
}
This is some sample data:
{
_index: providers
_type: provider
_id: 3
_version: 1
_score: 1
_source: {
locations:
id: 3
profile: <p>Dr Murray is a (blah blah)</p>
cost_id: 3
ages: null
nationwide: no
accreditations: null
service_types: null
individual_name: Dr Linley Murray
organisation_name: Crawford Medical Centre
languages: {"26":26}
regions: {"1":"Auckland"}
districts: {"8":"Manukau City"}
towns: {"2":"Howick"}
categories: {"10":10}
sub_categories: {"47":47}
funding_regions: {"7":7}
}
}
These are my indexing settings:
$index_settings = array(
'number_of_shards' => 5,
'number_of_replicas' => 1,
'analysis' => array(
'char_filter' => array(
'wise_mapping' => array(
'type' => 'mapping',
'mappings' => array('\'=>', '.=>', ',=>')
)
),
'filter' => array(
'wise_ngram' => array(
'type' => 'edgeNGram',
'min_gram' => 5,
'max_gram' => 10
)
),
'analyzer' => array(
'index_analyzer' => array(
'type' => 'custom',
'tokenizer' => 'standard',
'char_filter' => array('html_strip', 'wise_mapping'),
'filter' => array('standard', 'wise_ngram')
),
'search_analyzer' => array(
'type' => 'custom',
'tokenizer' => 'standard',
'char_filter' => array('html_strip', 'wise_mapping'),
'filter' => array('standard', 'wise_ngram')
),
)
)
);
Is there a better way to filter/search this? The filter worked when I used snowball instead of nGram. Why is this?
You are querying the category field looking for term 9, but the category field is actually an object:
{ "category": { "10": 10 }}
So your filter should look like this instead:
{ "term": { "category.9": 9 }}
Why are you specifying the category in this way? You'll end up with a new field for every category, which you don't want.
There's another problem with the query part. You are querying multiple fields with multi_match and setting operator to and. A query for "brown fox":
{ "multi_match": {
"query": "brown fox",
"fields": [ "foo", "bar"]
}}
would be rewritten as:
{ "dis_max": {
"queries": [
{ "match": { "foo": { "query": "brown fox", "operator": "and" }}},
{ "match": { "bar": { "query": "brown fox", "operator": "and" }}}
]
}}
In other words: all terms must be present in the same field, not in any of the listed fields! This is clearly not what you are after.
This is quite a hard problem to solve. In fact, in v1.1.0 we will be adding new functionality to the multi_match query which will greatly help in this situation.
You can read about the new functionality on this page.