Mimic Elasticsearch MatchQuery

В настоящее время я пишу программу, которая в настоящее время использует elasticsearch в качестве базового индекса базы данных/поиска. Я хотел бы подражать функциональным /_search конечной точки /_search, которая в настоящее время использует запрос соответствия:

{
    "query": {
        "match" : {
            "message" : "Neural Disruptor"
        }
    }
}

Выполнение некоторых выборочных запросов дало следующие результаты в массивной базе данных World of Warcraft:

   Search Term          Search Result      
------------------ ----------------------- 
 Neural Disruptor   Neural Needler         
 Lovly bracelet     Ruby Bracelet          
 Lovely bracelet    Lovely Charm Bracelet  

Просмотрев документацию elasticsearch, я обнаружил, что запрос соответствия довольно сложный. Какой самый простой способ смоделировать запрос соответствия с помощью только lucene в java? (Кажется, что он выполняет нечеткое совпадение, а также ищет термины)

Импорт кода elasticsearch для MatchQuery (я считаю, org.elasticsearch.index.search.MatchQuery), похоже, не так просто. Он сильно встроен в Elasticsearch и не похож на то, что можно легко вытащить.

Мне не нужно полное доказательство "Должно соответствовать точно, что соответствует матчу elasticsearch", мне просто нужно что-то близкое, или это может нечеткое совпадение/найти лучший матч.

Ответы

Ответ 1

Прошло некоторое время с тех пор, как я работал напрямую с lucene, но то, что вы хотите, должно быть, изначально довольно простым. Базовое поведение запроса lucene очень похоже на запрос соответствия (query_string точно эквивалентен lucene, но совпадение очень близко). Я собрал небольшой пример, который работает только с lucene (7.2.1), если вы хотите попробовать. Основной код выглядит следующим образом:

public static void main(String[] args) throws Exception {
    // Create the in memory lucence index
    RAMDirectory ramDir = new RAMDirectory();

    // Create the analyzer (has default stop words)
    Analyzer analyzer = new StandardAnalyzer();

    // Create a set of documents to work with
    createDocs(ramDir, analyzer);

    // Query the set of documents
    queryDocs(ramDir, analyzer);
}

private static void createDocs(RAMDirectory ramDir, Analyzer analyzer) 
        throws IOException {
    // Setup the configuration for the index
    IndexWriterConfig config = new IndexWriterConfig(analyzer);
    config.setOpenMode(IndexWriterConfig.OpenMode.CREATE);

    // IndexWriter creates and maintains the index
    IndexWriter writer = new IndexWriter(ramDir, config);

    // Create the documents
    indexDoc(writer, "document-1", "hello planet mercury");
    indexDoc(writer, "document-2", "hi PLANET venus");
    indexDoc(writer, "document-3", "howdy Planet Earth");
    indexDoc(writer, "document-4", "hey planet MARS");
    indexDoc(writer, "document-5", "ayee Planet jupiter");

    // Close down the writer
    writer.close();
}

private static void indexDoc(IndexWriter writer, String name, String content) 
        throws IOException {
    Document document = new Document();
    document.add(new TextField("name", name, Field.Store.YES));
    document.add(new TextField("body", content, Field.Store.YES));

    writer.addDocument(document);
}

private static void queryDocs(RAMDirectory ramDir, Analyzer analyzer) 
        throws IOException, ParseException {
    // IndexReader maintains access to the index
    IndexReader reader = DirectoryReader.open(ramDir);

    // IndexSearcher handles searching of an IndexReader
    IndexSearcher searcher = new IndexSearcher(reader);

    // Setup a query
    QueryParser parser = new QueryParser("body", analyzer);
    Query query = parser.parse("hey earth");

    // Search the index
    TopDocs foundDocs = searcher.search(query, 10);
    System.out.println("Total Hits: " + foundDocs.totalHits);

    for (ScoreDoc scoreDoc : foundDocs.scoreDocs) {
        // Get the doc from the index by id
        Document document = searcher.doc(scoreDoc.doc);
        System.out.println("Name: " + document.get("name") 
                + " - Body: " + document.get("body") 
                + " - Score: " + scoreDoc.score);
    }

    // Close down the reader
    reader.close();
}

Важные части распространения этого собирается быть анализатор и понимание Lucene синтаксиса анализатор запросов.

Analyzer используется как индексированием, так и запросами, чтобы указать, как разбирать текст, чтобы они могли думать о тексте одинаково. Он устанавливает, как tokenize (что нужно разделить, будь то toLower() и т.д.). StandardAnalyzer разбивается на пробелы и несколько других (у меня нет этой возможности), а также выглядит применительно к Lower().

QueryParser собирается выполнить некоторые из этих работ для вас. Если вы видите выше в моем примере. Я делаю две вещи, я говорю синтаксическому анализатору, что такое поле по умолчанию, и передаю строку hey earth. Парсер собирается превратить это в запрос, который выглядит как body:hey body:earth. Это будет искать документы, которые имеют либо hey либо earth в body. Два документа будут найдены.

Если мы пройдем hey AND earth запрос будет разобран, чтобы выглядеть как +body:hey +body:earth которого требуется, чтобы документы имели оба термина. Нулевые документы будут найдены.

Чтобы применить нечеткие опции, вы добавляете ~ к условиям, которые вы хотите быть нечеткими. Так что если запрос hey~ earth он применит нечеткость к hey и запрос будет выглядеть как body:hey~2 body:earth. Три документа будут найдены.

Вы можете напрямую писать запросы, и синтаксический анализатор все еще обрабатывает все. Так что если вы передаете его hey name:\"document-1\" (это лексемы расщепляется на -) будет создать запрос как body:hey name:"document 1". Два документа будут возвращены, поскольку он ищет фразу document 1 (так как она все еще токенизирует на -). Где, если бы я сделал hey name:document-1 он пишет body:hey (name:document name:1) который возвращает все документы, поскольку все они имеют document в качестве термина. Здесь есть некоторые нюансы.


Я попытаюсь немного подробнее рассказать о том, как они похожи. Ссылка на запрос соответствия. Elastic говорит, что основное различие будет: "Он не поддерживает префиксы имени поля, подстановочные знаки или другие" расширенные "функции". Вероятно, они выйдут из другого направления.

И запрос соответствия, и запрос lucene, при работе с анализируемым полем, будут брать строку запроса и применять к ней анализатор (tokenize it, toLower и т.д.). Поэтому они оба превратят HEY Earth в запрос, который ищет термины hey или earth.

Запрос соответствия может установить operator, предоставив "operator": "and". Это изменит наш запрос, чтобы искать hey и earth. Аналогия в lucene заключается в том, чтобы сделать что-то вроде parser.setDefaultOperator(QueryParser.Operator.AND);

Следующее - нечеткость. Оба работают с одинаковыми настройками. Я считаю, что эластичная "fuzziness": "AUTO" эквивалентна lucene auto при применении ~ к запросу (хотя, я думаю, вам нужно добавить каждый термин, который немного громоздкий).

Запрос с нулевыми терминами представляется эластичной конструкцией. Если вам нужен параметр ALL, вам придется реплицировать совпадение со всем запросом, если анализатор запросов удалил все токены из запроса.

Обрезание часто выглядит связанным с CommonTermsQuery. Я не использовал это, поэтому у вас может быть какое-то копание, если вы хотите его использовать.

У Lucene есть фильтр синонимов, который будет применен к анализатору, но вам, возможно, потребуется построить карту самостоятельно.


Различия, которые вы можете найти, вероятно, будут забиты. Когда я бегу, они спрашивают, зачем hey earth против люцина. Он получает документы-3 и документ-4, которые возвращаются со счетом 1.3862944. Когда я запускаю запрос в форме:

curl -XPOST http://localhost:9200/index/_search?pretty -d '{
  "query" : {
    "match" : {
      "body" : "hey earth"
    }
  }
}'

Я получаю те же документы, но со счетом 1.219939. Вы можете запустить объяснение для обоих из них. В lucene путем печати каждого документа с помощью

System.out.println(searcher.explain(query, scoreDoc.doc));

И в эластичном состоянии, запрашивая каждый документ, как

curl -XPOST http://localhost:9200/index/docs/3/_explain?pretty -d '{
  "query" : {
    "match" : {
      "body" : "hey earth"
    }
  }
}'

Я получаю некоторые отличия, но я не могу точно объяснить их. Я действительно получаю значение для документа 1.3862944 но fieldLength отличается и влияет на вес.

Ответ 2

Независимо от того, что отправлено в параметр q= конечной точки _search используется как и запрос query_string (не org.elasticsearch.index.search.MatchQuery), который понимает синтаксис выражения Lucene.

Синтаксис синтаксического анализа запроса определяется в проекте Lucene с использованием JavaCC, и здесь вы можете найти грамматику, если хотите посмотреть. Конечным продуктом является класс QueryParser (см. Ниже).

Класс внутри исходного кода ES, который отвечает за разбор строки запроса, представляет собой QueryStringQueryParser который делегирует класс Lucene QueryParser (сгенерированный JavaCC).

В принципе, если вы получите эквивалентную строку запроса, как передается _search?q=..., тогда вы можете использовать эту строку запроса с QueryParser.parse("query-string-goes-here") и запустить reified Query используя только Lucene.