ElasticSearch - практическое введение

ElasticSearch - поисковый движок с JSON REST API, использующий под капотом Lucene. Предлагаю смастерить с помощью Elastic поиск для парочки фильмов. На первом этапе рассмотрим установку и настройка Elastic в Docker контейнере, впрочем это опционально - вы можете установить Elastic любым другим способом. Затем копнём поглубже в процесс индексации, поисковые запросы, подсветку вхождений в результатах выдачи и агрегацию данных в Elastic и всё это только лишь с помощью REST API без необходимости наличия каких-либо глубоких познаний как в самом Lucene так и в программировании в целом !

UI ДЕМО

Для лучшего понимания изложенного материала набросал простенький UI (исходники) на ReactJS. Для работы демо из браузера нужно на сервере ElasticSearch, который мы сейчас настроим, разрешить http.cors например allow-origin: /https?:\/\/proiot\.ru/, либо просто звёздочка * - разрешить отовсюду.

screenshot

TL;DR

Предполагается что ElasticSearch уже установлен и настроен:

~$ curl -XGET localhost:9200
# response
{
  "name" : "J2",
  "cluster_name" : "docker-cluster",
  "version" : {
    "number" : "2.3.3",
    "build_hash" : "218bdf10790eef486ff2c41a3df5cfa32dadcfde",
    "build_timestamp" : "2016-05-17T15:40:04Z",
    "build_snapshot" : false,
    "lucene_version" : "5.5.0"
  },
  "tagline" : "You Know, for Search"
}

Достаточно просто проиндексировать содержимое дампа Top.bulk.json.xz:

# field mapping for suggest, aggregation
# curl -XDELETE 'localhost:9200/kinopoisk?pretty'
~$ curl -XPUT 'localhost:9200/kinopoisk?pretty' -d '{
  "mappings": {
    "film" : {
      "properties" : {
        "suggest" : { 
          "type" : "completion",
          "analyzer": "standard"
        },
        "category": {
          "type": "string",
          "fields": {
              "raw": {
                  "type": "string",
                  "index": "not_analyzed"
              }
          }
        },
        "directed": {
          "type": "string",
          "fields": {
              "raw": {
                  "type": "string",
                  "index": "not_analyzed"
              }
          }
        },
        "starring": {
          "type": "string",
          "fields": {
              "raw": {
                  "type": "string",
                  "index": "not_analyzed"
              }
          }
        }
      }
    }
  }
}'

# index 14992 docs(verify: echo `xzcat Top.bulk.json.xz | wc -l` / 2 | bc -l)
~$ xzcat Top.bulk.json.xz | curl -XPOST localhost:9200/_bulk --data-binary @-

Поля category, directed, starring в терминологии Elastic являются multi-field, в нашем случае они дополнительно хранят оригинальные значения что позволит агрегировать и фильтровать выдачу по строгому совпадению - например режиссёр "Игорь Масленников" вместо "Игорь" и "Масленников" по отдельности. После индексации собственно можно осуществлять поиск с подсветкой, сортировкой, агрегацией и фильтрацией, к осознанию которого было бы здорово подойти по окончании данного материала:

~$ curl -XGET 'localhost:9200/kinopoisk/film/_search?pretty' -d ' {
  "fields" : ["rate"],
  "query" : {
    "bool": {
      "must": [
        {
          "query_string" : {
            "query":    "шерлок", 
            "fields": [ "name", "description" ],
            "default_operator": "and"
          }
        }
      ],
      "filter": [
        {
          "match": {
            "directed.raw": "Игорь Масленников"
          }
        },
        {
          "bool": {
            "must": [
              {
                "match": {
                  "category.raw": "детектив"
                }
              },
              {
                "match": {
                  "category.raw": "криминал"
                }
              }
            ]
          }
        },
        {
          "range" : {
            "date" : {
              "gte" : "1980",
              "lt" : "1980||+1y",
              "format": "yyyy-MM||yyyy"
            }
          }
        }
      ]
    }
  },
  "highlight" : {
    "pre_tags" : ["[mazko.github.io]"],
    "post_tags" : ["[/mazko.github.io]"],
    "fields" : {
      "name" : {"fragment_size" : 22, "number_of_fragments" : 1},
      "description" : {"fragment_size" : 22, "number_of_fragments" : 1}
    }
  },
  "sort" : [
      {"rate" : {"order" : "desc"} }
  ],
  "aggs": {
    "years": {
      "date_histogram": {
        "field": "date",
        "interval": "year",
        "min_doc_count": 1,
        "order" : { "_count" : "desc" }
      },
      "aggs": {
        "months": {
          "date_histogram": {
            "field": "date",
            "interval": "month",
            "min_doc_count": 1,
            "order" : { "_count" : "desc" }
          }
        }
      }
    },
    "categories" : {
      "terms" : { "field" : "category.raw", "size": 0 }
    },
    "directors" : {
      "terms" : { "field" : "directed.raw", "size": 0 }
    },
    "stars" : {
      "terms" : { "field" : "starring.raw", "size": 0 }
    }
  }
}'
# response
{
  "took" : 3117,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 5,
    "max_score" : null,
    "hits" : [ {
      "_index" : "kinopoisk",
      "_type" : "film",
      "_id" : "77269",
      "_score" : null,
      "fields" : {
        "rate" : [ 8.415 ]
      },
      "highlight" : {
        "name" : [ "[mazko.github.io]Шерлок[/mazko.github.io] Холмс и доктор" ],
        "description" : [ " [mazko.github.io]Шерлок[/mazko.github.io] Холмс. Это" ]
      },
      "sort" : [ 8.415 ]
    }, {
      "_index" : "kinopoisk",
      "_type" : "film",
      "_id" : "354799",
      "_score" : null,
      "fields" : {
        "rate" : [ 8.313 ]
      },
      "highlight" : {
        "name" : [ "[mazko.github.io]Шерлок[/mazko.github.io] Холмс и доктор" ],
        "description" : [ "[mazko.github.io]Шерлок[/mazko.github.io] Холмс вступил" ]
      },
      "sort" : [ 8.313 ]
    }, {
      "_index" : "kinopoisk",
      "_type" : "film",
      "_id" : "77265",
      "_score" : null,
      "fields" : {
        "rate" : [ 8.297 ]
      },
      "highlight" : {
        "name" : [ "[mazko.github.io]Шерлок[/mazko.github.io] Холмс и доктор" ],
        "description" : [ ". Во второй — [mazko.github.io]Шерлок[/mazko.github.io]" ]
      },
      "sort" : [ 8.297 ]
    }, {
      "_index" : "kinopoisk",
      "_type" : "film",
      "_id" : "368937",
      "_score" : null,
      "fields" : {
        "rate" : [ 8.269 ]
      },
      "highlight" : {
        "description" : [ " водопада. [mazko.github.io]Шерлок[/mazko.github.io] Холмс" ]
      },
      "sort" : [ 8.269 ]
    }, {
      "_index" : "kinopoisk",
      "_type" : "film",
      "_id" : "368936",
      "_score" : null,
      "fields" : {
        "rate" : [ 8.248 ]
      },
      "highlight" : {
        "name" : [ "[mazko.github.io]Шерлок[/mazko.github.io] Холмс и доктор" ],
        "description" : [ " угрозой. [mazko.github.io]Шерлок[/mazko.github.io] Холмс" ]
      },
      "sort" : [ 8.248 ]
    } ]
  },
  "aggregations" : {
    "directors" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [ {
        "key" : "Игорь Масленников",
        "doc_count" : 5
      } ]
    },
    "categories" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [ {
        "key" : "детектив",
        "doc_count" : 5
      }, {
        "key" : "криминал",
        "doc_count" : 5
      } ]
    },
    "stars" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [ {
        "key" : "Василий Ливанов",
        "doc_count" : 5
      }, {
        "key" : "Виталий Соломин",
        "doc_count" : 5
      }, {
        "key" : "Рина Зеленая",
        "doc_count" : 5
      }, {
        "key" : "Борислав Брондуков",
        "doc_count" : 4
      }, {
        "key" : "Борис Клюев",
        "doc_count" : 3
      }, {
        "key" : "Александр Захаров",
        "doc_count" : 2
      }, {
        "key" : "Алексей Кожевников",
        "doc_count" : 2
      }, {
        "key" : "Валентина Панина",
        "doc_count" : 2
      }, {
        "key" : "Виктор Евграфов",
        "doc_count" : 2
      }, {
        "key" : "Игорь Дмитриев",
        "doc_count" : 2
      }, {
        "key" : "Игорь Ефимов",
        "doc_count" : 2
      }, {
        "key" : "Николай Крюков",
        "doc_count" : 2
      }, {
        "key" : "Адольф Ильин",
        "doc_count" : 1
      }, {
        "key" : "Анатолий Подшивалов",
        "doc_count" : 1
      }, {
        "key" : "Борис Рыжухин",
        "doc_count" : 1
      }, {
        "key" : "Виктор Аристов",
        "doc_count" : 1
      }, {
        "key" : "Виталий Баганов",
        "doc_count" : 1
      }, {
        "key" : "Геннадий Богачёв",
        "doc_count" : 1
      }, {
        "key" : "Игнат Лейрер",
        "doc_count" : 1
      }, {
        "key" : "Катерина Шелл",
        "doc_count" : 1
      }, {
        "key" : "Мария Соломина",
        "doc_count" : 1
      }, {
        "key" : "Николай Караченцов",
        "doc_count" : 1
      }, {
        "key" : "Светлана Крючкова",
        "doc_count" : 1
      }, {
        "key" : "Федор Одиноков",
        "doc_count" : 1
      } ]
    },
    "years" : {
      "buckets" : [ {
        "key_as_string" : "1980-01-01T00:00:00.000Z",
        "key" : 315532800000,
        "doc_count" : 5,
        "months" : {
          "buckets" : [ {
            "key_as_string" : "1980-01-01T00:00:00.000Z",
            "key" : 315532800000,
            "doc_count" : 3
          }, {
            "key_as_string" : "1980-03-01T00:00:00.000Z",
            "key" : 320716800000,
            "doc_count" : 2
          } ]
        }
      } ]
    }
  }
}

В запросе мы искали строку шерлок по полям name и description, исключили все фильмы кроме режиссёра "Игорь Масленников", жанры дожны быть только и детектив и криминал, только 1980 год. Сортировка найденных результатов по полю rate. Простая агрегация по полям category, directed и starring. Вложенная агрегация по дате - годы и в них месяцы. Но обо всём по порядку :)

УСТАНОВКА И НАСТРОЙКА ELASTICSEARCH

Предполагается что в системе уже установлен Docker:

~$ docker -v
Docker version 1.11.2, build b9f10c9

~$ docker pull ubuntu && docker run -it ubuntu

root@d1b1c0c6bddc:/# cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04 LTS"

root@d1b1c0c6bddc:/# apt update && apt -y upgrade && \
  apt -y install software-properties-common command-not-found \
  man-db openjdk-8-jdk wget curl net-tools

root@d1b1c0c6bddc:/# wget -qO - https://packages.elastic.co/GPG-KEY-elasticsearch | apt-key add -

root@d1b1c0c6bddc:/# echo "deb https://packages.elastic.co/elasticsearch/2.x/debian stable main" |\
  tee -a /etc/apt/sources.list.d/elasticsearch-2.x.list

root@d1b1c0c6bddc:/# apt -y install apt-transport-https && apt update && apt install -y elasticsearch

root@d1b1c0c6bddc:/# cd /usr/share/elasticsearch/
root@d1b1c0c6bddc:/# mkdir config/ && chown elasticsearch:elasticsearch config/
root@d1b1c0c6bddc:/# mkdir logs/ && chown elasticsearch:elasticsearch logs/
root@d1b1c0c6bddc:/# mkdir data/ && chown elasticsearch:elasticsearch data/

root@d1b1c0c6bddc:/# cat <<EOT > /usr/share/elasticsearch/config/logging.yml
rootLogger: TRACE,console
appender:
  console:
    type: console
    layout:
      type: consolePattern
      conversionPattern: "[%d{ISO8601}][%-5p][%-25c] %m%n"
EOT

root@d1b1c0c6bddc:/# cat <<EOT > /usr/share/elasticsearch/config/elasticsearch.yml
cluster.name: "docker-cluster"
network.host: 0.0.0.0
http.max_content_length: 333mb
http.cors:
  enabled: true 
  allow-origin: /https?:\/\/localhost(:[0-9]+)?/
  allow-methods: OPTIONS, HEAD, GET, POST, PUT, DELETE
  allow-headers: X-Requested-With,X-Auth-Token,Content-Type, Content-Length
EOT

root@d1b1c0c6bddc:/# exit

В настройках мы добавили поддержку http.cors чтобы с elastic можно было работать напрямую из браузера. На этом установка и настройка завершена - осталось зафиксировать изменения и запустить образ:

~$ docker commit d1b1c0c6bddc elastic
~$ docker rm d1b1c0c6bddc

~$ docker run --rm -it -u elasticsearch -p 9200:9200 elastic \
  /usr/share/elasticsearch/bin/elasticsearch

Проверка http.cors:

~$ curl -I -H 'Origin: http://localhost:8080' 'http://localhost:9200/'
# response
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:8080
Content-Type: text/plain; charset=UTF-8
Content-Length: 0

~$ curl -I -H 'Origin: http://some.hacker.com' 'http://localhost:9200/'
# response
HTTP/1.1 403 Forbidden

В настойках http.cors нет упоминаний о some.hacker.com поэтому справедливо 403. Наконец если мы хотим чтобы данные работы образа elastic сохранялись после его перезапуска:

~$ docker volume create --name elastic-data

# user: elasticsearch, port forward 9200, image: elastic, volume: elastic-data
~$ docker run --rm -it -u elasticsearch -p 9200:9200 \
  -v elastic-data:/usr/share/elasticsearch/data elastic \
  /usr/share/elasticsearch/bin/elasticsearch

С Docker всё.

REST API

Индексация:

~$ curl -XPOST 'localhost:9200/kinopoisk/film?pretty' -d {'
  "id": 409640,
  "name": "Касл (сериал 2009 – ...)",
  "description": "Знакомьтесь, Ричард Касл — успешный писатель детективного жанра...",
  "date": "2009-03-08T22:00:00.000Z",
  "rate": 8.042,
  "starring": [
    "Нэйтан Филлион",
    "Стана Катик",
    "Сьюзэн Салливан",
    "Джон Уэртас",
    "Шеймус Девер",
    "Молли К. Куинн",
    "Тамала Джонс",
    "Пенни Джонсон",
    "Рубен Сантьяго-Хадсон",
    "Майя Стоян"
  ],
  "category": [
    "драма",
    "мелодрама",
    "комедия",
    "криминал",
    "детектив"
  ],
  "directed": [
    "Роб Боумен",
    "Джон Терлески",
    "Билл Роу"
  ]
}'
# response
{
  "_index" : "kinopoisk",
  "_type" : "film",
  "_id" : "AVWHlznJTgOouHAvlXph",
  "_version" : 1,
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "created" : true
}

~$ curl -XPOST 'localhost:9200/kinopoisk/film?pretty' -d {'
  "id": 153013,
  "name": "Звездный крейсер Галактика (сериал 2004 – 2009)",
  "description": "Чудом уцелев после нападения Сайлонов...",
  "date": "2004-10-17T21:00:00.000Z",
  "rate": 7.943,
  "starring": [
    "Эдвард Джеймс Олмос",
    "Мэри МакДоннелл",
    "Джейми Бамбер",
    "Джеймс Кэллис",
    "Триша Хелфер",
    "Грейс Пак",
    "Кэти Сакхофф",
    "Майкл Хоган",
    "Аарон Дуглас",
    "Тамо Пеникетт"
  ],
  "category": [
    "фантастика",
    "боевик",
    "драма",
    "приключения"
  ],
  "directed": [
    "Майкл Раймер",
    "Майкл Нанкин",
    "Род Харди"
  ]
}'
# response
{
  "_index" : "kinopoisk",
  "_type" : "film",
  "_id" : "AVWHnN7ATgOouHAvlXpl",
  "_version" : 1,
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "created" : true
}

~$ curl -XGET 'localhost:9200/kinopoisk/_mapping?pretty'
# response
{
  "kinopoisk" : {
    "mappings" : {
      "film" : {
        "properties" : {
          "category" : {
            "type" : "string"
          },
          "date" : {
            "type" : "date",
            "format" : "strict_date_optional_time||epoch_millis"
          },
          "description" : {
            "type" : "string"
          },
          "directed" : {
            "type" : "string"
          },
          "id" : {
            "type" : "long"
          },
          "name" : {
            "type" : "string"
          },
          "rate" : {
            "type" : "double"
          },
          "starring" : {
            "type" : "string"
          }
        }
      }
    }
  }
}

ElasticSearch автоматически и даже правильно распознал типы данных для всех полей в индексе ! Простой постраничный поиск по двум полям name и description, в результатах поиска выводить только значения полей id, rate:

~$ curl -XGET 'localhost:9200/kinopoisk/film/_search?pretty' -d {'
    "fields" : ["id", "rate"],
    "query" : {
      "query_string" : {
        "query":    "Касл", 
        "fields": [ "name", "description" ],
        "default_operator": "and"
      }
    }
}'
# response
{
  "took" : 8,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 0.15342641,
    "hits" : [ {
      "_index" : "kinopoisk",
      "_type" : "film",
      "_id" : "AVWHmwbSTgOouHAvlXpi",
      "_score" : 0.15342641,
      "fields" : {
        "rate" : [ 8.042 ],
        "id" : [ 409640 ]
      }
    } ]
  }
}

Поиск с подсветкой:

~$ curl -XGET 'localhost:9200/kinopoisk/film/_search?pretty' -d {'
    "fields" : ["id", "rate"],
    "query" : {
      "query_string" : {
        "query":    "сайлон*", 
        "fields": [ "name", "description" ],
        "default_operator": "and"
      }
    },
    "highlight" : {
      "pre_tags" : ["[mazko.github.io]"],
      "post_tags" : ["[/mazko.github.io]"],
      "fields" : {
        "name" : {"fragment_size" : 22, "number_of_fragments" : 1},
        "description" : {"fragment_size" : 22, "number_of_fragments" : 1}
      }
    }
}'
# response
{
  "took" : 218,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 1.0,
    "hits" : [ {
      "_index" : "kinopoisk",
      "_type" : "film",
      "_id" : "AVWHnN7ATgOouHAvlXpl",
      "_score" : 1.0,
      "fields" : {
        "rate" : [ 7.943 ],
        "id" : [ 153013 ]
      },
      "highlight" : {
        "description" : [ " нападения [mazko.github.io]Сайлонов[/mazko.github.io]..." ]
      }
    } ]
  }
}

Самое время для реальных данных:

~: curl -XDELETE 'localhost:9200/kinopoisk?pretty'

~: time python3 -c \
  'import sys, json; [print(json.dumps(i, ensure_ascii=False)) for i in json.load(sys.stdin)]' \
  < Top.json | xargs -d '\n' -n1 curl -XPOST 'localhost:9200/kinopoisk/film/' -d

# time
real  18m52.728s
user  1m36.896s
sys   0m36.216s

Процесс индексации можно значительно ускорить используя Bulk API:

~: curl -XDELETE 'localhost:9200/kinopoisk?pretty'

~: time python3 -c \
  'import sys, json; [print(json.dumps({ "index" : { "_index" : "kinopoisk", "_type" : "film" } }),
  "\n", json.dumps(i, ensure_ascii=False)) for i in json.load(sys.stdin)]' \
   < Top.json | curl -XPOST localhost:9200/_bulk --data-binary @-

# time
real  0m14.450s
user  0m1.044s
sys   0m0.196s

Немного полезной статистики об индексе:

~: curl -XGET 'localhost:9200/kinopoisk/_count?pretty'
# response
{
  "count" : 14999,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  }
}

~: curl -XGET 'localhost:9200/_cat/indices'
# response
yellow open kinopoisk 5 1 14999 0 39.1mb 39.1mb

Suggest в ElasticSearch довольно странный либо плохо описанный:

~: curl -XDELETE 'localhost:9200/kinopoisk?pretty'

~: curl -XPUT 'localhost:9200/kinopoisk?pretty' -d '{
  "settings": {
    "analysis": {
      "filter": {
        "shingle_filter": {
          "type": "shingle",
          "max_shingle_size": "5",
          "min_shingle_size": "2",
          "output_unigrams":"true"
        }
      },
      "analyzer": {
        "pre_suggest_analyzer": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "shingle_filter"
          ]
        }
      }
    }
  }
}'

~: curl -XGET 'localhost:9200/kinopoisk/_analyze?pretty' -d '{
  "analyzer": "pre_suggest_analyzer",
  "text": ["раз два", "раз"]
}'
#response
{
  "tokens" : [ {
    "token" : "раз",
    "start_offset" : 0,
    "end_offset" : 3,
    "type" : "<ALPHANUM>",
    "position" : 0
  }, {
    "token" : "раз два",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "shingle",
    "position" : 0
  }, {
    "token" : "два",
    "start_offset" : 4,
    "end_offset" : 7,
    "type" : "<ALPHANUM>",
    "position" : 1
  }, {
    "token" : "раз",
    "start_offset" : 8,
    "end_offset" : 11,
    "type" : "<ALPHANUM>",
    "position" : 102
  } ]
}

Исходя из документации несовсем понятно как в процессе индексации сделать такую штуку автоматической без избыточных HTTP запросов к localhost:9200/kinopoisk/_analyze:

~: python3 -c '
import sys, json
from urllib.request import urlopen

def get_shingles(*args):
  data = { "analyzer": "pre_suggest_analyzer", "text": args }
  data = json.dumps(data).encode("utf8")
  with urlopen("http://localhost:9200/kinopoisk/_analyze", data=data) as r:
    data = json.loads(r.read().decode(r.info().get_param("charset") or "utf-8"))
    tokens = map(lambda v: v["token"], data["tokens"])
    tokens = filter(lambda v: len(v) > 1, tokens)
    return list(set(tokens))

for film in { obj["id"]: obj for obj in json.load(sys.stdin) }.values():
  film["rate"] = float(film["rate"])
  film["suggest"] = { "input": get_shingles(film["name"], film["description"]) }
  print(json.dumps({ "index" : { "_index" : "kinopoisk", "_type" : "film", "_id" : film["id"]} })) 
  print(json.dumps(film, ensure_ascii=False))
' < Top.json | xz --extreme -9 > Top.bulk.json.xz

Индекс уже с Suggest:

~: curl -XDELETE 'localhost:9200/kinopoisk?pretty'

~: curl -XPUT 'localhost:9200/kinopoisk?pretty' -d '{
  "mappings": {
    "film" : {
      "properties" : {
        "suggest" : { 
          "type" : "completion",
          "analyzer": "standard"
          }
        }
      }
    }
  }
}'

~: xzcat Top.bulk.json.xz | curl -XPOST localhost:9200/_bulk --data-binary @-

~: curl -XGET 'localhost:9200/kinopoisk/_suggest?pretty' -d '{
  "film-suggest" : {
    "text" : "шерлок ",
    "completion" : {
        "field" : "suggest",
        "size": 5
    }
  }
}'
#response
{
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "film-suggest" : [ {
    "text" : "шерлок ",
    "offset" : 0,
    "length" : 7,
    "options" : [ {
      "text" : "шерлок холмс",
      "score" : 3.0
    }, {
      "text" : "шерлок холмс и",
      "score" : 2.0
    }, {
      "text" : "шерлок холмс и доктор",
      "score" : 2.0
    }, {
      "text" : "шерлок холмс и доктор ватсон",
      "score" : 2.0
    }, {
      "text" : "шерлок младший",
      "score" : 1.0
    } ]
  } ]
}

Поиск с агрегацией по полю starring:

~$ curl -XDELETE 'localhost:9200/kinopoisk?pretty'

~$ curl -XPUT 'localhost:9200/kinopoisk?pretty' -d '{
  "mappings": {
    "film" : {
      "properties" : {
        "starring": {
          "type": "string",
          "fields": {
              "raw": {
                  "type": "string",
                  "index": "not_analyzed"
              }
          }
        }
      }
    }
  }
}'

~$ xzcat Top.bulk.json.xz | curl -XPOST localhost:9200/_bulk --data-binary @-

~$ curl -XGET 'localhost:9200/kinopoisk/film/_search?pretty' -d {'
  "size": 0,
  "query" : {
    "query_string" : {
      "query":    "шерлок", 
      "fields": [ "name", "description" ],
      "default_operator": "and"
    }
  },
  "aggs": {
    "stars" : {
      "terms" : { "field" : "starring.raw", "size": 10 }
    }
  }
}'
# response
{
  "took" : 1103,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 16,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "stars" : {
      "doc_count_error_upper_bound" : 1,
      "sum_other_doc_count" : 122,
      "buckets" : [ {
        "key" : "Василий Ливанов",
        "doc_count" : 7
      }, {
        "key" : "Виталий Соломин",
        "doc_count" : 7
      }, {
        "key" : "Рина Зеленая",
        "doc_count" : 7
      }, {
        "key" : "Борислав Брондуков",
        "doc_count" : 6
      }, {
        "key" : "Борис Клюев",
        "doc_count" : 4
      }, {
        "key" : "Александр Захаров",
        "doc_count" : 2
      }, {
        "key" : "Алексей Кожевников",
        "doc_count" : 2
      }, {
        "key" : "Валентина Панина",
        "doc_count" : 2
      }, {
        "key" : "Виктор Евграфов",
        "doc_count" : 2
      }, {
        "key" : "Джеральдин Джеймс",
        "doc_count" : 2
      } ]
    }
  }
}

links

social