Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.44% covered (success)
94.44%
34 / 36
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
SemanticSearchQueryBuilder
94.44% covered (success)
94.44%
34 / 36
75.00% covered (warning)
75.00%
3 / 4
7.01
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 build
85.71% covered (warning)
85.71%
12 / 14
0.00% covered (danger)
0.00%
0 / 1
3.03
 buildQuery
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
2
 buildDegraded
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace CirrusSearch\Query;
4
5use CirrusSearch\Elastica\NeuralQuery;
6use CirrusSearch\Search\SearchContext;
7use CirrusSearch\SearchConfig;
8use Elastica\Query\AbstractQuery;
9use Elastica\Query\InnerHits;
10use Elastica\Query\Nested;
11
12/**
13 * Query builder for semantic search using OpenSearch neural queries.
14 *
15 * This is a minimal implementation that skips most full-text search
16 * functionality and generates a simple neural query for vector similarity search.
17 * OpenSearch handles the embedding generation using the configured model.
18 *
19 * TODO: highlighting
20 */
21class SemanticSearchQueryBuilder implements FullTextQueryBuilder {
22    public const SYNTAX_NAME = 'semantic';
23
24    private string $nestedField;
25    private string $vectorField;
26    /** @var string[] */
27    private array $sourceFields;
28    private int $maxK;
29    private string $scoreMode;
30    private string $instructions;
31
32    /**
33     * @param SearchConfig $config Not used, but part of standard constructor
34     * @param KeywordFeature[] $features Not used, but part of standard constructor
35     * @param array $settings Configuration settings for the builder
36     *   - nested_field: The name of the nested field holding paragraph embeddings (default: 'passage_chunk_embedding')
37     *   - vector_field: The name of the vector sub-field to search (default: 'knn')
38     *   - source_fields: The name of the sub-fields to return (default: section, text)
39     *   - maxK: Maximum number of requested results
40     *   - score_mode: ??? (default: max)
41     *   - instructions: If set, will be prepended to the query. (default: empty string)
42     */
43    public function __construct( SearchConfig $config, array $features, array $settings = [] ) {
44        $this->nestedField = $settings['nested_field'] ?? 'passage_chunk_embedding';
45        $this->vectorField = $settings['vector_field'] ?? 'knn';
46        $this->sourceFields = $settings['source_fields'] ?? [ 'section', 'text' ];
47        $this->maxK = $settings['k'] ?? 21;
48        $this->scoreMode = $settings['score_mode'] ?? 'max';
49        $this->instructions = $settings['instructions'] ?? '';
50    }
51
52    /**
53     * Build a neural query for the supplied term.
54     *
55     * @param SearchContext $searchContext
56     * @param string $term term to search
57     */
58    public function build( SearchContext $searchContext, $term ) {
59        $searchContext->addSyntaxUsed( self::SYNTAX_NAME );
60
61        $term = trim( $term );
62        if ( $term === '' ) {
63            $searchContext->addWarning( 'cirrussearch-semantic-empty-query' );
64            $searchContext->setResultsPossible( false );
65            return;
66        }
67
68        $query = $searchContext->getSearchQuery();
69        $k = $query->getMaximumResultPosition();
70        if ( $k > $this->maxK ) {
71            $searchContext->addWarning( 'cirrussearch-semantic-too-many-results', $this->maxK );
72            $k = $this->maxK;
73        }
74
75        // Clean search term is (currently) only used by the RescoreBuilder,
76        // which we don't invoke for semantic search, but maybe someday we
77        // will.
78        $searchContext->setCleanedSearchTerm( $term );
79        // We don't have the suggest field on semantic indices
80        $searchContext->disableFallbackRunner();
81
82        $searchContext->setMainQuery( $this->buildQuery( $term, $k ) );
83    }
84
85    private function buildQuery( string $term, int $k ): AbstractQuery {
86        $source = [];
87        foreach ( $this->sourceFields as $field ) {
88            $source[] = "{$this->nestedField}.{$field}";
89        }
90        return ( new Nested() )
91            ->setPath( $this->nestedField )
92            ->setQuery( new NeuralQuery(
93                "{$this->nestedField}.{$this->vectorField}",
94                $this->instructions . $term,
95                $k
96            ) )
97            ->setScoreMode( $this->scoreMode )
98            ->setInnerHits( ( new InnerHits() )
99                ->setSize( 1 )
100                ->setSource( $source )
101            );
102    }
103
104    /**
105     * Semantic search does not support degraded queries.
106     *
107     * @param SearchContext $searchContext
108     * @return bool Always returns false
109     */
110    public function buildDegraded( SearchContext $searchContext ) {
111        return false;
112    }
113}