Consolidate SQL for distance_function into one single source of truth - #21
Conversation
…truth. Vector search from three independent implementations drifted apart. The new WPVDB/Search Class contains shared logic for the three.
There was a problem hiding this comment.
Pull request overview
This pull request introduces a unified WPVDB\\Search service as the single source of truth for vector similarity search SQL and fallback behavior, and updates REST, WP_Query integration, and the embeddings admin screen to delegate to it. This reduces drift across call sites and centralizes distance-function selection via Database::get_vector_distance_function().
Changes:
- Added
WPVDB\\Search::query()service to centralize native-vs-fallback search behavior, SQL construction, and result shape. - Updated REST and WP_Query vector search paths to call the new service (including REST over-fetch and consistent thresholding in fallback where applicable).
- Updated embeddings admin screen semantic search to delegate to the service and request
explaindata.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| wpvdb.php | Loads the new Search service during plugin bootstrap. |
| includes/class-wpvdb-search.php | New unified search implementation (native SQL + PHP fallback) and filterable clauses. |
| includes/class-wpvdb-rest.php | Delegates REST query handling to Search::query() and adds over-fetch constant. |
| includes/class-wpvdb-query.php | Delegates WP_Query vector search to Search::query() and applies similarity threshold to fallback too. |
| admin/views/embeddings.php | Delegates admin semantic search to Search::query() with explain enabled. |
| tests/bootstrap.php | Extends the wpdb stub for posts table name and loads the Search class for tests. |
| tests/unit/SearchTest.php | Adds unit tests covering Search defaults, clause construction, filterability, and input validation. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| $limit = $query->get( 'posts_per_page' ); | ||
| $limit = $limit ? $limit : 10; | ||
| Logger::debug( 'Posts per page limit: ' . $limit ); | ||
|
|
||
| $doc_ids = array(); | ||
|
|
||
| if ( $has_vector ) { | ||
| try { | ||
| // Convert the embedding array to JSON. | ||
| $embedding_json = wp_json_encode( $embedding ); | ||
|
|
||
| // Use Database class to get the appropriate vector function. | ||
| $vector_function = self::$database->get_vector_from_string_function( $embedding_json ); | ||
| Logger::debug( 'Using vector function: ' . $vector_function ); | ||
|
|
||
| // Use Database class to get the appropriate distance function. | ||
| $distance_function = self::$database->get_vector_distance_function( 'embedding', $vector_function, 'cosine' ); | ||
| Logger::debug( 'Using distance function: ' . $distance_function ); | ||
|
|
||
| // Set an appropriate similarity threshold - we discovered this is critical for performance | ||
| // Lower values (0.2-0.3) are more strict but faster, higher values (0.4-0.6) give more results. | ||
| $similarity_threshold = apply_filters( 'wpvdb_similarity_threshold', 0.35 ); | ||
|
|
||
| // Optimized query that uses the vector index with a distance threshold. | ||
| // The threshold + ORDER BY + LIMIT pattern is what maximizes vector index usage. | ||
| $sql = $wpdb->prepare( | ||
| " | ||
| SELECT doc_id, | ||
| $distance_function AS distance | ||
| FROM $table_name | ||
| WHERE $distance_function < %f | ||
| AND model = %s | ||
| ORDER BY distance | ||
| LIMIT %d | ||
| ", | ||
| $similarity_threshold, | ||
| $model, | ||
| $limit * 3 // fetch more candidates than needed. | ||
| ); | ||
|
|
||
| Logger::debug( 'Vector search SQL: ' . $sql ); | ||
|
|
||
| $rows = $wpdb->get_results( $sql, ARRAY_A ); | ||
|
|
||
| if ( $wpdb->last_error ) { | ||
| Logger::error( 'Database error in vector search: ' . $wpdb->last_error ); | ||
| } | ||
|
|
||
| if ( $rows ) { | ||
| Logger::debug( 'Found ' . count( $rows ) . ' results from vector search' ); | ||
| foreach ( $rows as $r ) { | ||
| $doc_ids[] = (int) $r['doc_id']; | ||
| Logger::debug( 'Added doc_id: ' . $r['doc_id'] . ' with distance: ' . $r['distance'] ); | ||
| } | ||
| } else { | ||
| Logger::debug( 'No results found from vector search' ); | ||
| } | ||
| } catch ( \Exception $e ) { | ||
| Logger::error( 'Exception in vector search: ' . $e->getMessage() ); | ||
| } | ||
| } else { | ||
| Logger::debug( 'No vector support, using PHP fallback search' ); | ||
| // Fallback: do in PHP. | ||
| $all_rows = $wpdb->get_results( | ||
| $wpdb->prepare( "SELECT doc_id, embedding FROM $table_name WHERE model = %s", $model ), | ||
| ARRAY_A | ||
| ); | ||
| $distances = array(); | ||
| foreach ( $all_rows as $r ) { | ||
| $stored_emb = json_decode( $r['embedding'], true ); | ||
| if ( ! is_array( $stored_emb ) ) { | ||
| continue; | ||
| } | ||
| $d = REST::cosine_distance( $embedding, $stored_emb ); | ||
| $distances[] = array( | ||
| 'doc_id' => (int) $r['doc_id'], | ||
| 'distance' => $d, | ||
| ); | ||
| } | ||
| usort( | ||
| $distances, | ||
| function ( $a, $b ) { | ||
| return $a['distance'] <=> $b['distance']; | ||
| } | ||
| ); | ||
| $distances = array_slice( $distances, 0, $limit * 3 ); | ||
| $doc_ids = wp_list_pluck( $distances, 'doc_id' ); | ||
|
|
| add_filter( | ||
| 'wpvdb_search_clauses', | ||
| function ( $clauses ) { | ||
| $clauses['where'][] = 'e.doc_type = %s'; | ||
| $clauses['params'][] = 'product'; | ||
| return $clauses; | ||
| } | ||
| ); | ||
|
|
||
| $clauses = Search::build_clauses( | ||
| $this->args( | ||
| array( | ||
| 'model' => 'm', | ||
| 'respect_visibility' => false, | ||
| ) | ||
| ) | ||
| ); | ||
|
|
||
| $this->assertSame( array( 'e.model = %s', 'e.doc_type = %s' ), $clauses['where'] ); | ||
| $this->assertSame( array( 'm', 'product' ), $clauses['params'] ); | ||
| } |
There was a problem hiding this comment.
The repo already solves this globally. phpunit.xml:22 registers an extension:
<extension class="WPVDB_Filter_Reset_Hook" file="tests/filter-reset-hook.php" />
which implements BeforeTestHook and AfterTestHook and clears the registry around every test
Also removed logger that's storing vectorized data, quickly increasing the weight of debug.log. Finally, fixed indentation issue on SQL.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Suppressed comments (2)
includes/class-wpvdb-search.php:158
Search::query()treats any array passed invectoras a valid embedding without validation. Because this is a public service API (and the REST layer allows vector queries behind a capability gate), passing an empty vector, wrong dimension, or non-numeric/non-finite values can lead to SQL errors in the native path and inconsistent scoring in the PHP fallback. Normalize and validate the vector before proceeding, returning a 400WP_Errorwhen invalid.
$embedding = $args['vector'];
if ( ! is_array( $embedding ) ) {
if ( '' === trim( (string) $args['text'] ) ) {
return new \WP_Error(
'wpvdb_search_no_input',
__( 'A query text or a query vector is required.', 'wpvdb' ),
array( 'status' => 400 )
);
}
$started = microtime( true );
$embedding = self::resolve_embedding( $args );
$plan['timings_ms']['embed'] = (int) round( ( microtime( true ) - $started ) * 1000 );
if ( is_wp_error( $embedding ) ) {
return $embedding;
}
}
includes/class-wpvdb-search.php:167
$planincludes acandidateskey but it is never populated, even though the code computes$fetch = $limit * $over_fetch. This makes the returned plan inconsistent and less useful for debugging/explain output.
$fetch = $args['limit'] * $args['over_fetch'];
Vector search exists in three independent implementations that have drifted apart:
REST::handle_queryLEFT JOIN wp_posts)COSINE_DISTANCE()Query::maybe_vector_searchlimit * 3COSINE_DISTANCE()admin/views/embeddings.phpDISTANCE(..., 'COSINE')Each carried its own native + PHP-fallback branches, its own SQL, and its own
thresholds.
This is done as a prerequisite for adding filtered search, ie: constraining results by post
type, taxonomy, author, date.
What changed
New
WPVDB\Searchservice inincludes/class-wpvdb-search.php: