Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
94.44% |
34 / 36 |
|
75.00% |
3 / 4 |
CRAP | |
0.00% |
0 / 1 |
| SemanticSearchQueryBuilder | |
94.44% |
34 / 36 |
|
75.00% |
3 / 4 |
7.01 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
| build | |
85.71% |
12 / 14 |
|
0.00% |
0 / 1 |
3.03 | |||
| buildQuery | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
2 | |||
| buildDegraded | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace CirrusSearch\Query; |
| 4 | |
| 5 | use CirrusSearch\Elastica\NeuralQuery; |
| 6 | use CirrusSearch\Search\SearchContext; |
| 7 | use CirrusSearch\SearchConfig; |
| 8 | use Elastica\Query\AbstractQuery; |
| 9 | use Elastica\Query\InnerHits; |
| 10 | use 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 | */ |
| 21 | class 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 | } |