I'm currently using Laravel v7.2, have the babenkoivan/scout-elasticsearch-driver installed (4.2) and am using AWS Elasticsearch 7.1. I have several tables mapped in my application that are working fine but am having issues with a nested mapping that was previously working and is now broken.
I'm saving data into a table and having that table data copied into AWS Elasticsearch. I'm using MySQL 5.6 so I am using a TEXT column to store JSON data. Data in the table looks as follows:
'id' => 1,
'results' => [{"finish":1,"other_id":1,"other_data":1}]
I have my model setup with the following mapping:
protected $mapping = [
'properties' => [
'results' => [
'type' => 'nested',
'properties' => [
'finish' => [
'type' => 'integer'
],
'other_id' => [
'type' => 'integer'
],
'other_data' => [
'type' => 'integer'
]
]
],
]
];
And if it's of any use, the toSearchableArray:
public function toSearchableArray()
{
$array = [
'id' => $this->id,
'results' => $this->results
];
return $array;
}
I have no problem creating this index and it worked up until about a couple of months ago. I don't know exactly when, as it wasn't a high priority item and may have occurred around an AWS ES update but not sure why this in particular would break. I receive the following error now:
{"error":{"root_cause":[{"type":"mapper_parsing_exception","reason":"object mapping for [results] tried to parse field [results] as object, but found a concrete value"}],"type":"mapper_parsing_exception","reason":"object mapping for [results] tried to parse field [results] as object, but found a concrete value"},"status":400}
I've tried also storing the data in the table as such, thinking it was breaking due to the potential array, but it was to no avail:
'id' => 1,
'results' => {"finish":1,"other_id":1,"other_data":1}
I'm at a loss for what else to try to get this working again.
EDIT: Here is the entire model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use ScoutElastic\Searchable;
class ResultsModel extends Model
{
use Searchable;
protected $indexConfigurator = \App\MyIndexConfiguratorResults::class;
protected $searchRules = [
//
];
protected $mapping = [
'properties' => [
'results' => [
'type' => 'nested',
'properties' => [
'finish' => [
'type' => 'integer'
],
'other_id' => [
'type' => 'integer'
],
'other_data' => [
'type' => 'integer'
]
]
],
]
];
public function searchableAs()
{
return 'results_index';
}
public function toSearchableArray()
{
$array = [
'id' => $this->id,
'results' => $this->results
];
return $array;
}
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'results_table';
}
Here is the \App\MyIndexConfiguratorResults::class
<?php
namespace App;
use ScoutElastic\IndexConfigurator;
use ScoutElastic\Migratable;
class MyIndexConfiguratorResults extends IndexConfigurator
{
use Migratable;
protected $name = "results_index";
/**
* #var array
*/
protected $settings = [
//
];
}
This is all that is needed to have Laravel automatically update AWS ES each time the table is updated. For the initial load, I would SSH in and run the following command to have it create the index. This, as well as elastic:migrate and any update/insert into the table produces the mapping error.
php artisan elastic:create-index results_index
Finally figured this out so will share the solution for anyone that runs into this. Turns out to be a fairly simple fix, though I'm not sure how it even worked in the first place so that part is still baffling.
I created a brand new index and update the mappings accordingly to add "id" and remove the type "nested" from the "results" piece. (Adding the "nested" type was adding two "results" to the index - one that contained all my nested data, the other just being "object".)
protected $mapping = [
'properties' => [
'id' => [
'type' => 'integer'
],
'results' => [
'properties' => [
'finish' => [
'type' => 'integer'
],
'other_id' => [
'type' => 'integer'
],
'other_data' => [
'type' => 'integer'
]
]
],
]
];
Then I simply added json_decode to the toSearchableArray() function as so:
public function toSearchableArray()
{
$array = [
'id' => $this->id,
'results' => json_decode($this->results, true)
];
return $array;
}
Voila. It successfully created the index and imported the data in a manner with which I can query the nested object.
Reading through the docs, the type field seems to have been removed. Scroll down to 7.x to see. Also, it seems you need to delete the index and re-add it in order for the new map to work according to this page.
Related
How can i hide some fields ?
i want to hide the file field
Eloquent :
$reports = Report::select('id', 'file','company_id', 'title', 'year', 'created_at')
->orderBy('id', 'desc')
->paginate(10);
return ReportResource::collection($reports);
Model :
...
public function getFileSizeAttribute()
{
return Storage::disk('files')->size($this->attributes['file']);
}
....
ReportResource:
...
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'year' => $this->year,
'views' => $this->whenNotNull($this->views),
'file' => $this->whenNotNull($this->file), <-- i want to hide the file field
'file_size' => $this->fileSize, <-- but always show file_size
'created_at' => $this->created_at,
'company' => new CompanyResource($this->company),
];
}
to get file_size field i must select the file field , because it's depends on it to calculate the file size.
but i want to hide the file field before send the response.
i know i can use the protected $hidden = [] method in the model , but i don't want that, because file field it's required on others place. i just want to hide it on this endpoint only.
Since you are using API resources the best and clean way to do this is by using a Resource class for your collection.
Said that, you will have 3 Resources:
The first one, as it is, just for retrieving a single Model with file and file_size attributes. The one you already have ReportResource.php
...
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'year' => $this->year,
'views' => $this->whenNotNull($this->views),
'file' => $this->whenNotNull($this->file),
'file_size' => $this->fileSize,
'created_at' => $this->created_at,
'company' => new CompanyResource($this->company),
];
}
A new second resource to be used in your endpoint, without the file attribute. IE: ReportIndexResource.php
...
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'year' => $this->year,
'views' => $this->whenNotNull($this->views),
'file_size' => $this->fileSize,
'created_at' => $this->created_at,
'company' => new CompanyResource($this->company),
];
}
Now you need to create a Resource collection which explicitly defines the Model Resource to use. IE: ReportCollection.php
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class ReportCollection extends ResourceCollection
{
/**
* The resource that this resource collects.
*
* #var string
*/
public $collects = ReportIndexResource::class;
}
Finally, use this new resource collection in your endpoint
$reports = Report::select('id', 'file','company_id', 'title', 'year', 'created_at')
->orderBy('id', 'desc')
->paginate(10);
return new ReportCollection($reports);
Of course, you can make use of makeHidden() method, but IMO is better to write a little more code and avoid a non desired attribute in your response because you forgot to make it hidden.
Also, in case you make use of makeHidden() method and you want to show the attribute in a future, you will have to update all your queries instead of a silgle resource file.
If you want to make it Hide From All Returns , you can Do this in model
protected $hidden = ['file'];
and if you want to do it temporirly with this query , you can Use MakeHidden method
$users = $reports->makeHidden(['file']);
It's clear in laravel docs , take a look
https://laravel.com/docs/9.x/eloquent-collections#method-makeHidden
I use Laravel-Metable package in my project. This package return collection object using in key meta name and on value eloquent object.
Here you can see package data result screenshot.
How I can override result data and get this type of array data:
$meta = [
[
'id' => 1,
'key' => "Meta Name",
'value' => "Meta Value"
],
[
'id' => 2,
'key' => "Meta Name",
'value' => "Meta Value"
],
];
I will load my models meta with lazy loading:
use Metable;
protected $with = ['meta'];
You can use the collection map method for that so it should be something like this:
$result = $metaItems->map(function($meta) {
return [
'id' => $meta->id,
'key' => $meta->key,
'value' => $meta->value
];
})->values();
// then $result->toArray(); should give you the expected result
I updated my yii2 system from yii2-elasticsearch 2.0 to 2.1 and elasticsearch package from 2.2.1 to 6.2.1. In the old system I could mix $query->andFilterWhere and $query->query as follows (the search method is in a class derived from yii\elasticsearch\ActiveRecord):
public function search($params)
{
$query = self::find();
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
$this->load($params);
$query->andFilterWhere([
'languageCode' => \Yii::$app->locale->languageCode,
]);
$queryPart = [];
if (!empty($this->term)) {
$queryPart['filtered']['query']['multi_match'] = [
// ES6: $queryPart['bool']['must']['multi_match'] = [
'query' => $this->term,
'operator' => 'and',
'type' => $this->getQueryType($this->term),
'fields' => [
'name_*',
'meta_description_*'
]
];
}
if (!empty($queryPart)) {
$query->query($queryPart);
}
return $dataProvider;
}
It worked with ES 2.2.1 without any problem, but now the andFilterWhere overwrites $query->query independently from the sequence. If one of the two parts is removed the other filter works perfectly, only together not.
Any idea?
You must use bool query and put all part of your query in one "query" object...
Something like this:
query => [
bool => [
must => [
multi_match => [
'query' => $this->term,
'operator' => 'and',
'type' => $this->getQueryType($this->term),
'fields' => [
'name_*',
'meta_description_*'
]
]
]
filter => [
'languageCode' => \Yii::$app->locale->languageCode
]
]
]
This problem seems to be bug, as confirmed by other users on github.
I have Gridview and one column value I get via http request. Is there a way to sort the table by this column?
myTableModel.php
class myTableModel extends \yii\db\ActiveRecord
{
...,
public function getExternalValue() {
$client = new Client();
return $client->createRequest()->setMethod('get')
->setUrl('http:://...')->setData(['id' => 1])->send()->content;
}
}
myTableModelSearch.php
class myTableModelSearch extends myTableModel
{
public function rules()
{
return [
[[...,'externalValue'], 'string'],
[[..., 'externalValue'], 'safe']
];
}
public $externalValue;
public function searchView($params) {
$query = SomeTable::find();
$dataProvider = new ActiveDataProvider(['query' => $query]);
$dataProvider->setSort(['attributes' => [
'externalValue' => [
'asc' => ['externalValue' => SORT_ASC],
'desc' => ['externalValue' => SORT_DESC]
]
]]);
if (!($this->load($params) && $this->validate()))
return $dataProvider;
return $dataProvider;
}
}
view.php
GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [
... ,
[
'attribute' => 'externalValue',
'value' => function($item) {
return $item->externalValue;
},
]
],
]);
I also tried to add value to view simply with $item->getExternalValue() (and without public property set), but it makes no difference - when trying to sort I get database exception error SQLSTATE[42S22]: Column not found: 1054 Unknown column 'externalValue' in 'order clause'. How could I trick gridview, to make it sort my table by externalValue column?
You're using yii\data\ActiveDataProvider which uses an instance of ActiveQuery to find its data.
Try using yii\data\ArrayDataProvider, or extend yii\data\ActiveDataProvider to allow a second source for your data.
Additionally, you have to implement a sort function that can sort using your attribute.
see more here and here
I have a very simply model which I want to paginate. The pagination works but the sort links do not have any effect:
My Controller:
public $paginate = [
'limit' => 10,
'order' => [
'Properties.id' => 'asc'
],
'sortWhitelist' => [
'Properties.id',
'Properties.name',
'Properties.active'
],
];
My query:
$properties = $this->Properties->find('all')->where($options)->contain($contains)->order(['Properties.id']);
$this->set('properties', $this->paginate($properties));
My view displays 10 items per page and the links to pages/next/previous work fine. When I click the sort link:
$this->Paginator->sort('id', 'ID')
The url called is:
properties/index/3?sort=id&direction=desc
The page re-loads but the order of the data does not change.
The whitelist expects the exact field name that you are using in sort. It will not automatically prepend the primary table's name, so your whitelist should look like this:
public $paginate = [
'limit' => 10,
'order' => [
'Properties.id' => 'asc'
],
'sortWhitelist' => [
'id',
'name',
'active'
],
];
It will see "id" in the query and check that the exact field name "id" exists in the whitelist.
Also, it appears you are including an order() on your query. To allow the paginator to set the order, you should remove this clause.