laravel / scout

Laravel Scout provides a driver based solution to searching your Eloquent models.
https://laravel.com/docs/scout
MIT License
1.55k stars 331 forks source link

Driver [elasticsearch] not supported. #143

Closed excellentingenuity closed 7 years ago

excellentingenuity commented 7 years ago

This is probably because Taylor is working on it right now but even with composer requiring the elasticsearch/elasticsearch package scout is unable to find the elastic search driver. I cannot find any other search results with this particular error so I am raising it here. Using: "laravel/scout": "^2.0", "elasticsearch/elasticsearch": "^5.0" On: Laravel 5.3.28

corydemille commented 7 years ago

It looks like as of 3 days ago, the elasticsearch engine was pulled from the repo looking at the commit history...

DMeganoski commented 7 years ago

Taylor said some time ago that he would not be providing further updates to the elasticsearch driver, that it was too much work to maintain in the core.

I can see why, elasticsearch seems to change their api quite frequently, and the results it will produce vary greatly depending on the methods used on the driver level.

There are several packages out there which support elasticsearch, here but in the end you would probably be best off writing your own driver based on your desired version of the elasticsearch php api and customize the indexes yourself.

Just for an example, here is my driver, based off the one that was in the scout repo. My driver utilizes another custom class which gathers all the mappings from the searchable models. It also uses a 'prefix' and 'ngram' filter for better typeahead matching. (the default driver returned way too many irrelevant results when searching simple things like usernames) You might be able to copy-paste this into your app, but I would suggest reading up on the php api docs and the underlying rest api and get farmiliar with how things work, first.

<?php

namespace App\Search;

use App\Builders\ElasticMapping;
use Elasticsearch\ClientBuilder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Collection as BaseCollection;
use Laravel\Scout\Builder;
use Laravel\Scout\Engines\Engine;

class ElasticsearchEngine extends Engine {
    /**
     * The Elasticsearch client instance.
     *
     * @var \Elasticsearch\Client
     */
    protected $elasticsearch;

    /**
     * The index name.
     *
     * @var string
     */
    protected $index;

    /**
     * Create a new engine instance.
     *
     * @param  \Elasticsearch\Client $elasticsearch
     * @return void
     */
    public function __construct() {

        $client = ClientBuilder::create()->build();
        $this->elasticsearch = $client;

        $this->index = config('scout.elasticsearch5.index');
    }

    /**
     * Update the given model in the index.
     *
     * @param  Collection $models
     * @return void
     */
    public function update($models) {

        $body = new BaseCollection();

        $models->each(function ($model) use ($body) {
            $array = $model->toSearchableArray();

            if (empty($array)) {
                return;
            }

            if (!$this->elasticsearch->indices()->exists(['index' => $this->index])) {
                $params = [
                    'index' => $this->index,
                    'body' => [
                        'settings' => [
                            'number_of_shards' => 3,
                            'number_of_replicas' => 2,
                            "analysis" => [
                                "filter" => [
                                    "ngram_filter" => [
                                        "type" => "ngram",
                                        "min_gram" => 3,
                                        "max_gram" => 8
                                    ]
                                ],
                                "analyzer" => [
                                    "index_ngram" => [
                                        "type" => "custom",
                                        "tokenizer" => "keyword",
                                        "filter" => ["ngram_filter", "lowercase"]
                                    ],
                                    "search_ngram" => [
                                        "type" => "custom",
                                        "tokenizer" => "keyword",
                                        "filter" => "lowercase"
                                    ]
                                ],
                            ],
                        ],
                        'mappings' => ElasticMapping::gather(),
                    ]
                ];
                $this->elasticsearch->indices()->create($params);
            }

            $body->push([
                'index' => [
                    '_index' => $this->index,
                    '_type' => $model->searchableAs(),
                    '_id' => $model->getKey(),
                ],
            ]);

            $body->push($array);
        });

        $this->elasticsearch->bulk([
            'refresh' => true,
            'body' => $body->all()
        ]);
    }

    /**
     * Remove the given model from the index.
     *
     * @param  Collection $models
     * @return void
     */
    public function delete($models) {
        $body = new BaseCollection();

        $models->each(function ($model) use ($body) {
            $body->push([
                'delete' => [
                    '_index' => $this->index,
                    '_type' => $model->searchableAs(),
                    '_id' => $model->getKey(),
                ],
            ]);
        });

        $this->elasticsearch->bulk([
            'refresh' => true,
            'body' => $body->all(),
        ]);
    }

    /**
     * Perform the given search on the engine.
     *
     * @param  Builder $query
     * @return mixed
     */
    public function search(Builder $query) {
        return $this->performSearch($query, [
            'filters' => $this->filters($query),
            'size' => $query->limit ?: 10000,
        ]);
    }

    /**
     * Perform the given search on the engine.
     *
     * @param  Builder $builder
     * @param  array $options
     * @return mixed
     */
    protected function performSearch(Builder $builder, array $options = []) {
        $filters = [];

        $term = strtolower($builder->query);

        foreach (explode(' ', $term) as $piece) {
            $matches[] = [
                'prefix' => ["_all" => $piece]
            ];
        }

        if (array_key_exists('filters', $options) && $options['filters']) {
            foreach ($options['filters'] as $field => $value) {

                if (is_numeric($value)) {
                    $filters[] = [
                        'term' => [
                            $field => $value,
                        ],
                    ];
                } elseif (is_string($value)) {
                    $matches[] = [
                        'match' => [
                            $field => [
                                'query' => $value,
                                'operator' => 'and'
                            ]
                        ]
                    ];
                }
            }
        }

        $query = [
            'index' => $this->index,
            'type' => $builder->model->searchableAs(),
            'body' => [
                'query' => [
                    'bool' => [
                        'filter' => $filters,
                        'must' => $matches,
                    ],
                ],
            ],
        ];

        if (array_key_exists('size', $options)) {
            $query['size'] = $options['size'];
        }

        if (array_key_exists('from', $options)) {
            $query['from'] = $options['from'];
        }

        if ($builder->callback) {
            return call_user_func(
                $builder->callback,
                $this->elasticsearch,
                $query
            );
        }

        return $this->elasticsearch->search($query);
    }

    /**
     * Get the filter array for the query.
     *
     * @param  Builder $query
     * @return array
     */
    protected function filters(Builder $query) {
        return $query->wheres;
    }

    /**
     * Perform the given search on the engine.
     *
     * @param  Builder $query
     * @param  int $perPage
     * @param  int $page
     * @return mixed
     */
    public function paginate(Builder $query, $perPage, $page) {
        $result = $this->performSearch($query, [
            'filters' => $this->filters($query),
            'size' => $perPage,
            'from' => (($page * $perPage) - $perPage),
        ]);

        $result['nbPages'] = (int)ceil($result['hits']['total'] / $perPage);
        //dd($result);
        return $result;
    }

    /**
     * Map the given results to instances of the given model.
     *
     * @param  mixed $results
     * @param  \Illuminate\Database\Eloquent\Model $model
     * @return Collection|BaseCollection
     */
    public function map($results, $model) {
        if (count($results['hits']) === 0) {
            return Collection::make();
        }

        $keys = collect($results['hits']['hits'])
            ->pluck('_id')
            ->values()
            ->all();

        $models = $model->whereIn(
            $model->getQualifiedKeyName(), $keys
        )->get()->keyBy($model->getKeyName());
        return Collection::make($results['hits']['hits'])->map(function ($hit) use ($model, $models) {
            $record = isset($models[$hit['_source'][$model->getKeyName()]])
                ? $models[$hit['_source'][$model->getKeyName()]] : $model;
            $record->highlights = [];
            return $record;
        })->filter()->values();
    }

    /**
     * Get the total count from a raw result returned by the engine.
     *
     * @param  mixed $results
     * @return int
     */
    public function getTotalCount($results) {
        return $results['hits']['total'];
    }
}

Here is the class that I use to gather model mappings

<?php
/**
 * Created by PhpStorm.
 * User: darryl
 * Date: 12/9/2016
 * Time: 12:18 PM
 */

namespace App\Builders;

use App\Models\User;

class ElasticMapping {

    public static function gather() {

        $path = base_path() . '/app/Models/';

        $files = scandir($path);

        $mappings = [];

        foreach ($files as $name) {
            if ($name !== '.' && $name !== '..') {
                $class_name = '\App\Models\\' . substr($name, 0, strlen($name) - 4);

                $class = new $class_name;

                if (method_exists($class, 'search')) {

                    if (method_exists($class, 'getFieldMappings')) {
                        $mappings[$class->searchableAs()] = [
                            '_source' => [
                                'enabled' => true,
                            ],
                            'properties' => $class->getFieldMappings(),
                        ];
                    } else {
                        //$mappings[$class->searchableAs()] = [];
                    }
                }
            }
        }

        return $mappings;
    }

    public static function getModels() {
        $path = base_path() . '/app/Models/';

        $files = scandir($path);

        $classes = [];

        $operator = \Sentinel::getUser();

        foreach ($files as $name) {
            if ($name !== '.' && $name !== '..') {
                $class_name = '\App\Models\\' . substr($name, 0, strlen($name) - 4);

                $class = new $class_name;

                if (method_exists($class, 'search')) {
                    if (!$class->viewPermission || $operator->hasAccess($class->viewPermission)) {
                        $classes[$class->searchableAs()] = $class;
                    }
                }
            }
        }

        return $classes;
    }
}
prakash-chokalingam commented 7 years ago

Driver [elasticsearch] not supported.

am also facing this same issue

lukepolo commented 7 years ago

here is a sample of what i have https://gist.github.com/lukepolo/b63d303b076a7cd58bbaa54b3b9f0370

lukepolo commented 7 years ago

Client builder is part of the elastic search package I believe , I can't check it out as I'm not at my computer

shijunti19 commented 7 years ago

@lukepolo sorry ! Thank you I have found a solution! use Laravel\Scout\EngineManager; use Elasticsearch\ClientBuilder as ClientBuilder;

rashidashraf commented 7 years ago

Driver [elasticsearch] not supported. am also facing this same issue

Artistan commented 7 years ago

This is working well for us...

https://github.com/BrokerExchange/ElasticScout