Repository navigation
Search the operands of an OR in batches of SQL terms - #234
Merged
Merged
Conversation
search_doc_ids searched an OR one SQL query per operand. The operands that are single SQL terms are now searched up to 200 at a time in one query (search_or_operands): - over a whole branch, one SELECT per operand combined with UNION, so each keeps its index; - restricted to candidate documents, one restricted query with the operands' conditions joined by OR, which reads the candidates' rows once instead of once per operand. ORed base.id lookups among the operands are searched together by search_base_ids; ANDs and negated ORs are searched one at a time as before. TestBatchedOr compares the batched search with one query per operand for every operation the SQL builder supports, negated ones included, in mixed ORs and inside ANDs. Closes #233 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Z8rXBFDpnZo3FeCE3ymyZ
Contributor
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #234 +/- ##
==========================================
+ Coverage 66.91% 67.30% +0.39%
==========================================
Files 67 67
Lines 4748 4793 +45
==========================================
+ Hits 3177 3226 +49
+ Misses 1571 1567 -4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
#235 brute-forces the depends_on queries SQL cannot match exactly (isBruteForceDependsOn), and query_struct_to_sql_str now errors on them. search_or_operands keeps such operands out of its SQL batches, so they are searched one at a time by search_doc_ids, which brute-forces them. TestBatchedOr covers depends_on with an empty name, alone in an OR and inside an AND. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Z8rXBFDpnZo3FeCE3ymyZ
Resolving the merge with #235 dropped the 'end's that close the last loop of search_or_operands and the function, so it ran on into brute_force_search. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Z8rXBFDpnZo3FeCE3ymyZ
stevewds
approved these changes
Oct 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #233. DID-python counterpart: VH-Lab/DID-python#105 (closes VH-Lab/DID-python#104). Merge this one first.
Update: #235 merged in
#235 (exact
depends_onmatching) changed the same part ofdatabase.m, somainis merged into this branch (4f4e9aa, followed by the fixe4e02dc).depends_onthat SQL can't match exactly, such as one with an empty name, is now brute-forced (isBruteForceDependsOn→brute_force_search), andquery_struct_to_sql_strthrows an error for it.search_or_operandsnow keeps those operands out of its SQL batches, sosearch_doc_idsbrute-forces them one at a time.testBruteForceDependsOnInAnOrcoversdepends_onand~depends_onwith an empty name, in an OR and in an OR inside an AND.ends that closesearch_or_operands.e4e02dcputs them back. The mergeddatabase.mwas then compared with both sides: it removes nothing frommain, and from this branch it removes only the code Match depends_on in SQLite exactly as did.query does #235 replaced.What changed
did.database/search_doc_idsused to search an OR with one SQL query per operand, thenunionthe results in MATLAB. Now the OR's operands go to a new method,search_or_operands, which sorts them into three groups:depends_on) are searched up tomaxOrBatchTerms= 200 per SQL query. That covers every operationquery_struct_to_sql_strsupports, negated ones included.base.idlookups among the operands are collected and searched together withsearch_base_ids. That's the same batching an all-base.idOR already gets, used only whenhasSearchIndexes()is true.~ors and brute-forceddepends_onqueries are searched one at a time bysearch_doc_ids, as before.The batch size comes from a new protected method,
orBatchTerms(). The test helperQueryLogSqlitedbgets anorBatchSizeproperty that overrides it. A value of 1 gives the old one-query-per-operand path, which the tests use for comparison.The SQL, and why
Over a whole branch: one
SELECTper operand, joined withUNION. Each member is exactly the query the operand used to run on its own, built byget_sql_query_str.isakeeps itsLIMIT -1distinct-superclass subquery.Restricted to candidates (an OR inside an AND, after the AND has narrowed to at most 1,000 documents): one query using the restricted head (
CROSS JOIN…INDEXED BY doc_data_doc_idx…docs.doc_id IN (…)), with the operands' conditions joined byOR.WHEREthe rows are read once.CROSS JOIN/INDEXED BY, so the OR can't make SQLite fall back to scanning all ofdoc_data.Results are the same either way: a document matches when one of its
doc_datarows satisfies one of the conditions. The 200-term cap keeps a batch well below SQLite's default limits of 500 compound-select terms and expression depth 1000.Timings
Setup: synthetic DID database built with Python
sqlite33.45.1 — DID schema, 100,000 docs × 30 fields (3Mdoc_datarows), withdoc_data_value,doc_data_field_valueanddoc_data_doc_idx. The SQL is a Python mirror ofget_sql_query_str/query_struct_to_sql_str. Times are SQL execution only.Workloads:
base.nameexact_stringterms.exact_string,exact_number,depends_on,isa,exact_string_anycase,base.idandgreaterthanterms.Over a whole branch, the ORed
WHEREthat #233 worried about really does lose the indexes. It took ~1 s for 10 names, so it's out.run_sql_query's struct-to-cell conversion and the growingunion. That's 1,000 round trips becoming 5.greaterthan,exact_number). That cost is the same either way.Restricted to candidates, the ORed
WHEREis 10–50× faster than one query per term, before counting any MATLAB-side saving. VH-Lab/DID-python#105 has the same comparison timed on DID-python's realSQLiteDB: 1,000 names restricted to 1,000 candidates drop from 10.1 s to 0.23 s.Tests
New
tests/+did/+unittest/TestBatchedOr.m. Each comparison runs the search three ways — default batching, batches of 3 (to exercise chunking), and one query per operand — and checks the results are identical (verifyEqual, same order):testEveryOperationInAnOr: each of the 14 SQL operations, plus the negation of each that has one, ORed with other terms.testAllOperationsInOneOr: all of them in one OR. Also checks it runs as a single query.testEveryOperationInAnOrInsideAnAnd: the same insideisa demoB AND (…), which takes the restricted path. Also compared with the unrestricted search (noSearchIndexes), and checks the query count is 2.testMixedOr:base.idlookups, an AND operand, a nested~orand plain terms in one OR, alone and inside an AND. Checks the batched search runs fewer queries.testLongOrIsSearchedInBatches: 450 names take 3 queries instead of 450, and 1 + 3 when restricted.testBruteForceDependsOnInAnOr: a brute-forceddepends_on(empty name), plain and negated, in an OR and inside an AND.testWithoutSearchIndexes: the UNION path with no candidates and no id batching.Outside MATLAB, I checked the two SQL forms with Python
sqlite3(with aregex()function registered). Both returned exactly the per-term results for:~hasmember,~isa) in one restricted OR, which stays inside the expression-depth limit.Noticed, not changed
hasmemberwith a string value: it emitsdoc_data.value=<value>unquoted, so SQLite reads it as a column name ("no such column"). This happens on both paths. Only numeric values work today.~or:search_doc_idstreats it as a plainor(the~is stripped). This is unchanged, and a nested~orstill goes through that same path.Merge
Please merge with a merge commit, so
e4e02dc(the hash VH-Lab/DID-python#105's bridge records) stays reachable frommain. Then merge VH-Lab/DID-python#105.🤖 Generated with Claude Code
https://claude.ai/code/session_016Z8rXBFDpnZo3FeCE3ymyZ