birdclaw 0.9.3 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 0.9.4 - 2026-07-05
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- `search tweets` now returns in well under a second on large archives (previously ~60s, minutes for common terms): the FTS5 match set is materialized once instead of SQLite re-running the MATCH scan per timeline edge row, the join order adapts to term frequency, the LIMIT is resolved on an id-only inner query, and snippets are computed only for returned rows.
|
|
8
|
+
|
|
3
9
|
## 0.9.3 - 2026-07-04
|
|
4
10
|
|
|
5
11
|
### Changed
|
package/dist/cli/birdclaw.js
CHANGED
|
@@ -11166,7 +11166,8 @@ function getTimelineQualityReason(row, lowQualityThreshold) {
|
|
|
11166
11166
|
return "keep:long-text";
|
|
11167
11167
|
}
|
|
11168
11168
|
var RECENT_TIMELINE_EDGE_CANDIDATES = 5e3;
|
|
11169
|
-
|
|
11169
|
+
var FTS_DRIVE_FROM_MATCHES_MAX = 1e4;
|
|
11170
|
+
function buildTimelineItemsQuery({
|
|
11170
11171
|
resource,
|
|
11171
11172
|
account,
|
|
11172
11173
|
listAccountId,
|
|
@@ -11179,13 +11180,12 @@ function listTimelineItems({
|
|
|
11179
11180
|
includeReplies = true,
|
|
11180
11181
|
qualityFilter = "all",
|
|
11181
11182
|
lowQualityThreshold,
|
|
11182
|
-
includeQualityReason = false,
|
|
11183
11183
|
likedOnly = false,
|
|
11184
11184
|
bookmarkedOnly = false,
|
|
11185
11185
|
limit = 18
|
|
11186
|
-
}) {
|
|
11187
|
-
const db = getReadDb();
|
|
11186
|
+
}, ftsMatchCountHint = 0) {
|
|
11188
11187
|
const kind = resource === "mentions" ? "mention" : resource;
|
|
11188
|
+
const cteParams = [];
|
|
11189
11189
|
const params = [];
|
|
11190
11190
|
const normalizedLowQualityThreshold = normalizeLowQualityThreshold(lowQualityThreshold);
|
|
11191
11191
|
const shouldDedupeAcrossAccounts = !account || account === "all";
|
|
@@ -11198,9 +11198,8 @@ function listTimelineItems({
|
|
|
11198
11198
|
`;
|
|
11199
11199
|
const unwindowedTimelineEdgesCte = timelineEdgesCte;
|
|
11200
11200
|
let usedRecentEdgeWindow = false;
|
|
11201
|
-
let join3 = "";
|
|
11202
11201
|
let where = "where e.kind = ?";
|
|
11203
|
-
|
|
11202
|
+
const ftsSearch = search?.trim() ? toFtsSearchQuery(search) : "";
|
|
11204
11203
|
const canUseRecentEdgeWindow = !likedOnly && !bookmarkedOnly && !listId && !account && !search?.trim() && replyFilter === "all" && !since?.trim() && !until?.trim() && includeReplies && qualityFilter === "all";
|
|
11205
11204
|
if (likedOnly || bookmarkedOnly) {
|
|
11206
11205
|
if (likedOnly && bookmarkedOnly) {
|
|
@@ -11224,7 +11223,7 @@ function listTimelineItems({
|
|
|
11224
11223
|
where kind = ?
|
|
11225
11224
|
)
|
|
11226
11225
|
`;
|
|
11227
|
-
|
|
11226
|
+
cteParams.push(collectionKind2);
|
|
11228
11227
|
}
|
|
11229
11228
|
where = "where 1 = 1";
|
|
11230
11229
|
} else if (canUseRecentEdgeWindow) {
|
|
@@ -11246,11 +11245,11 @@ function listTimelineItems({
|
|
|
11246
11245
|
RECENT_TIMELINE_EDGE_CANDIDATES,
|
|
11247
11246
|
limit * 50
|
|
11248
11247
|
);
|
|
11249
|
-
|
|
11248
|
+
cteParams.push(kind, candidateLimit);
|
|
11250
11249
|
where = "where e.kind = ?";
|
|
11251
11250
|
params.push(kind);
|
|
11252
11251
|
} else {
|
|
11253
|
-
|
|
11252
|
+
cteParams.push(kind);
|
|
11254
11253
|
where = "where e.kind = ?";
|
|
11255
11254
|
params.push(kind);
|
|
11256
11255
|
}
|
|
@@ -11313,16 +11312,32 @@ function listTimelineItems({
|
|
|
11313
11312
|
`;
|
|
11314
11313
|
params.push(listAccountId, listId);
|
|
11315
11314
|
}
|
|
11316
|
-
const
|
|
11315
|
+
const ftsMatchesCte = ftsSearch ? `, fts_matches as materialized (
|
|
11316
|
+
select tweet_id
|
|
11317
|
+
from tweets_fts
|
|
11318
|
+
where tweets_fts.text match ?
|
|
11319
|
+
)` : "";
|
|
11317
11320
|
if (ftsSearch) {
|
|
11318
|
-
|
|
11319
|
-
where += " and tweets_fts.text match ?";
|
|
11320
|
-
searchSnippetSelect = ", snippet(tweets_fts, 1, '<mark>', '</mark>', '...', 16) as search_snippet";
|
|
11321
|
-
params.push(ftsSearch);
|
|
11321
|
+
cteParams.push(ftsSearch);
|
|
11322
11322
|
}
|
|
11323
|
+
const searchDrivenFrom = ftsMatchCountHint > FTS_DRIVE_FROM_MATCHES_MAX ? `tweets t
|
|
11324
|
+
cross join fts_matches on fts_matches.tweet_id = t.id
|
|
11325
|
+
cross join timeline_edges e on e.tweet_id = t.id` : `fts_matches
|
|
11326
|
+
cross join tweets t on t.id = fts_matches.tweet_id
|
|
11327
|
+
cross join timeline_edges e on e.tweet_id = t.id`;
|
|
11323
11328
|
params.push(limit);
|
|
11329
|
+
if (ftsSearch) {
|
|
11330
|
+
params.push(limit);
|
|
11331
|
+
}
|
|
11332
|
+
const searchSelectionCte = ftsSearch ? `, search_selection as materialized (
|
|
11333
|
+
select t.id as tweet_id, e.account_id, e.kind, e.raw_json
|
|
11334
|
+
from ${searchDrivenFrom}
|
|
11335
|
+
${where}
|
|
11336
|
+
order by t.created_at desc, t.id desc
|
|
11337
|
+
limit ?
|
|
11338
|
+
)` : "";
|
|
11324
11339
|
const buildTimelineSelectSql = (timelineEdgesSql) => `
|
|
11325
|
-
${timelineEdgesSql}
|
|
11340
|
+
${timelineEdgesSql}${ftsMatchesCte}${searchSelectionCte}
|
|
11326
11341
|
select
|
|
11327
11342
|
t.id,
|
|
11328
11343
|
e.account_id,
|
|
@@ -11399,8 +11414,7 @@ function listTimelineItems({
|
|
|
11399
11414
|
qp.avatar_hue as quoted_avatar_hue,
|
|
11400
11415
|
qp.avatar_url as quoted_avatar_url,
|
|
11401
11416
|
qp.created_at as quoted_profile_created_at
|
|
11402
|
-
|
|
11403
|
-
from timeline_edges e
|
|
11417
|
+
from ${ftsSearch ? "search_selection e" : "timeline_edges e"}
|
|
11404
11418
|
join tweets t on t.id = e.tweet_id
|
|
11405
11419
|
join accounts a on a.id = e.account_id
|
|
11406
11420
|
join profiles p on p.id = t.author_profile_id
|
|
@@ -11408,14 +11422,52 @@ function listTimelineItems({
|
|
|
11408
11422
|
left join profiles rp on rp.id = rt.author_profile_id
|
|
11409
11423
|
left join tweets qt on qt.id = t.quoted_tweet_id
|
|
11410
11424
|
left join profiles qp on qp.id = qt.author_profile_id
|
|
11411
|
-
${
|
|
11412
|
-
${where}
|
|
11425
|
+
${ftsSearch ? "" : where}
|
|
11413
11426
|
order by t.created_at desc, t.id desc
|
|
11414
11427
|
limit ?
|
|
11415
11428
|
`;
|
|
11416
|
-
|
|
11417
|
-
|
|
11418
|
-
|
|
11429
|
+
return {
|
|
11430
|
+
sql: buildTimelineSelectSql(timelineEdgesCte),
|
|
11431
|
+
params: [...cteParams, ...params],
|
|
11432
|
+
fallbackSql: buildTimelineSelectSql(unwindowedTimelineEdgesCte),
|
|
11433
|
+
fallbackParams: [kind, kind, limit],
|
|
11434
|
+
usedRecentEdgeWindow,
|
|
11435
|
+
ftsSearch
|
|
11436
|
+
};
|
|
11437
|
+
}
|
|
11438
|
+
var SEARCH_SNIPPET_SQL = "snippet(tweets_fts, 1, '<mark>', '</mark>', '...', 16)";
|
|
11439
|
+
function listTimelineItems(query) {
|
|
11440
|
+
const db = getReadDb();
|
|
11441
|
+
const {
|
|
11442
|
+
includeQualityReason = false,
|
|
11443
|
+
lowQualityThreshold,
|
|
11444
|
+
limit = 18
|
|
11445
|
+
} = query;
|
|
11446
|
+
const normalizedLowQualityThreshold = normalizeLowQualityThreshold(lowQualityThreshold);
|
|
11447
|
+
const ftsSearch = query.search?.trim() ? toFtsSearchQuery(query.search) : "";
|
|
11448
|
+
const ftsMatchCount = ftsSearch ? Number(
|
|
11449
|
+
db.prepare(
|
|
11450
|
+
"select count(*) as match_count from tweets_fts where tweets_fts.text match ?"
|
|
11451
|
+
).get(ftsSearch).match_count
|
|
11452
|
+
) : 0;
|
|
11453
|
+
const plan = buildTimelineItemsQuery(query, ftsMatchCount);
|
|
11454
|
+
let rows = db.prepare(plan.sql).all(...plan.params);
|
|
11455
|
+
if (plan.usedRecentEdgeWindow && rows.length < limit) {
|
|
11456
|
+
rows = db.prepare(plan.fallbackSql).all(...plan.fallbackParams);
|
|
11457
|
+
}
|
|
11458
|
+
if (plan.ftsSearch && rows.length > 0) {
|
|
11459
|
+
const snippetRows = db.prepare(
|
|
11460
|
+
`select tweet_id, ${SEARCH_SNIPPET_SQL} as search_snippet
|
|
11461
|
+
from tweets_fts
|
|
11462
|
+
where tweets_fts.text match ?
|
|
11463
|
+
and tweet_id in (${rows.map(() => "?").join(",")})`
|
|
11464
|
+
).all(plan.ftsSearch, ...rows.map((row) => String(row.id)));
|
|
11465
|
+
const snippetByTweetId = new Map(
|
|
11466
|
+
snippetRows.map((row) => [String(row.tweet_id), row.search_snippet])
|
|
11467
|
+
);
|
|
11468
|
+
for (const row of rows) {
|
|
11469
|
+
row.search_snippet = snippetByTweetId.get(String(row.id));
|
|
11470
|
+
}
|
|
11419
11471
|
}
|
|
11420
11472
|
const urlExpansionCache = /* @__PURE__ */ new Map();
|
|
11421
11473
|
const profileByHandleCache = /* @__PURE__ */ new Map();
|
|
@@ -8902,9 +8902,10 @@ function getTimelineQualityReason(row, lowQualityThreshold) {
|
|
|
8902
8902
|
return "keep:long-text";
|
|
8903
8903
|
}
|
|
8904
8904
|
var RECENT_TIMELINE_EDGE_CANDIDATES = 5e3;
|
|
8905
|
-
|
|
8906
|
-
|
|
8905
|
+
var FTS_DRIVE_FROM_MATCHES_MAX = 1e4;
|
|
8906
|
+
function buildTimelineItemsQuery({ resource, account, listAccountId, listId, search, replyFilter = "all", since, until, untilId, includeReplies = true, qualityFilter = "all", lowQualityThreshold, likedOnly = false, bookmarkedOnly = false, limit = 18 }, ftsMatchCountHint = 0) {
|
|
8907
8907
|
const kind = resource === "mentions" ? "mention" : resource;
|
|
8908
|
+
const cteParams = [];
|
|
8908
8909
|
const params = [];
|
|
8909
8910
|
const normalizedLowQualityThreshold = normalizeLowQualityThreshold(lowQualityThreshold);
|
|
8910
8911
|
const shouldDedupeAcrossAccounts = !account || account === "all";
|
|
@@ -8917,9 +8918,8 @@ function listTimelineItems({ resource, account, listAccountId, listId, search, r
|
|
|
8917
8918
|
`;
|
|
8918
8919
|
const unwindowedTimelineEdgesCte = timelineEdgesCte;
|
|
8919
8920
|
let usedRecentEdgeWindow = false;
|
|
8920
|
-
let join = "";
|
|
8921
8921
|
let where = "where e.kind = ?";
|
|
8922
|
-
|
|
8922
|
+
const ftsSearch = search?.trim() ? toFtsSearchQuery(search) : "";
|
|
8923
8923
|
const canUseRecentEdgeWindow = !likedOnly && !bookmarkedOnly && !listId && !account && !search?.trim() && replyFilter === "all" && !since?.trim() && !until?.trim() && includeReplies && qualityFilter === "all";
|
|
8924
8924
|
if (likedOnly || bookmarkedOnly) {
|
|
8925
8925
|
if (likedOnly && bookmarkedOnly) timelineEdgesCte = `
|
|
@@ -8942,7 +8942,7 @@ function listTimelineItems({ resource, account, listAccountId, listId, search, r
|
|
|
8942
8942
|
where kind = ?
|
|
8943
8943
|
)
|
|
8944
8944
|
`;
|
|
8945
|
-
|
|
8945
|
+
cteParams.push(collectionKind);
|
|
8946
8946
|
}
|
|
8947
8947
|
where = "where 1 = 1";
|
|
8948
8948
|
} else if (canUseRecentEdgeWindow) {
|
|
@@ -8961,11 +8961,11 @@ function listTimelineItems({ resource, account, listAccountId, listId, search, r
|
|
|
8961
8961
|
)
|
|
8962
8962
|
`;
|
|
8963
8963
|
const candidateLimit = Math.max(RECENT_TIMELINE_EDGE_CANDIDATES, limit * 50);
|
|
8964
|
-
|
|
8964
|
+
cteParams.push(kind, candidateLimit);
|
|
8965
8965
|
where = "where e.kind = ?";
|
|
8966
8966
|
params.push(kind);
|
|
8967
8967
|
} else {
|
|
8968
|
-
|
|
8968
|
+
cteParams.push(kind);
|
|
8969
8969
|
where = "where e.kind = ?";
|
|
8970
8970
|
params.push(kind);
|
|
8971
8971
|
}
|
|
@@ -9014,16 +9014,28 @@ function listTimelineItems({ resource, account, listAccountId, listId, search, r
|
|
|
9014
9014
|
`;
|
|
9015
9015
|
params.push(listAccountId, listId);
|
|
9016
9016
|
}
|
|
9017
|
-
const
|
|
9018
|
-
|
|
9019
|
-
|
|
9020
|
-
|
|
9021
|
-
|
|
9022
|
-
|
|
9023
|
-
|
|
9017
|
+
const ftsMatchesCte = ftsSearch ? `, fts_matches as materialized (
|
|
9018
|
+
select tweet_id
|
|
9019
|
+
from tweets_fts
|
|
9020
|
+
where tweets_fts.text match ?
|
|
9021
|
+
)` : "";
|
|
9022
|
+
if (ftsSearch) cteParams.push(ftsSearch);
|
|
9023
|
+
const searchDrivenFrom = ftsMatchCountHint > FTS_DRIVE_FROM_MATCHES_MAX ? `tweets t
|
|
9024
|
+
cross join fts_matches on fts_matches.tweet_id = t.id
|
|
9025
|
+
cross join timeline_edges e on e.tweet_id = t.id` : `fts_matches
|
|
9026
|
+
cross join tweets t on t.id = fts_matches.tweet_id
|
|
9027
|
+
cross join timeline_edges e on e.tweet_id = t.id`;
|
|
9024
9028
|
params.push(limit);
|
|
9029
|
+
if (ftsSearch) params.push(limit);
|
|
9030
|
+
const searchSelectionCte = ftsSearch ? `, search_selection as materialized (
|
|
9031
|
+
select t.id as tweet_id, e.account_id, e.kind, e.raw_json
|
|
9032
|
+
from ${searchDrivenFrom}
|
|
9033
|
+
${where}
|
|
9034
|
+
order by t.created_at desc, t.id desc
|
|
9035
|
+
limit ?
|
|
9036
|
+
)` : "";
|
|
9025
9037
|
const buildTimelineSelectSql = (timelineEdgesSql) => `
|
|
9026
|
-
${timelineEdgesSql}
|
|
9038
|
+
${timelineEdgesSql}${ftsMatchesCte}${searchSelectionCte}
|
|
9027
9039
|
select
|
|
9028
9040
|
t.id,
|
|
9029
9041
|
e.account_id,
|
|
@@ -9100,8 +9112,7 @@ function listTimelineItems({ resource, account, listAccountId, listId, search, r
|
|
|
9100
9112
|
qp.avatar_hue as quoted_avatar_hue,
|
|
9101
9113
|
qp.avatar_url as quoted_avatar_url,
|
|
9102
9114
|
qp.created_at as quoted_profile_created_at
|
|
9103
|
-
|
|
9104
|
-
from timeline_edges e
|
|
9115
|
+
from ${ftsSearch ? "search_selection e" : "timeline_edges e"}
|
|
9105
9116
|
join tweets t on t.id = e.tweet_id
|
|
9106
9117
|
join accounts a on a.id = e.account_id
|
|
9107
9118
|
join profiles p on p.id = t.author_profile_id
|
|
@@ -9109,13 +9120,40 @@ function listTimelineItems({ resource, account, listAccountId, listId, search, r
|
|
|
9109
9120
|
left join profiles rp on rp.id = rt.author_profile_id
|
|
9110
9121
|
left join tweets qt on qt.id = t.quoted_tweet_id
|
|
9111
9122
|
left join profiles qp on qp.id = qt.author_profile_id
|
|
9112
|
-
${
|
|
9113
|
-
${where}
|
|
9123
|
+
${ftsSearch ? "" : where}
|
|
9114
9124
|
order by t.created_at desc, t.id desc
|
|
9115
9125
|
limit ?
|
|
9116
9126
|
`;
|
|
9117
|
-
|
|
9118
|
-
|
|
9127
|
+
return {
|
|
9128
|
+
sql: buildTimelineSelectSql(timelineEdgesCte),
|
|
9129
|
+
params: [...cteParams, ...params],
|
|
9130
|
+
fallbackSql: buildTimelineSelectSql(unwindowedTimelineEdgesCte),
|
|
9131
|
+
fallbackParams: [
|
|
9132
|
+
kind,
|
|
9133
|
+
kind,
|
|
9134
|
+
limit
|
|
9135
|
+
],
|
|
9136
|
+
usedRecentEdgeWindow,
|
|
9137
|
+
ftsSearch
|
|
9138
|
+
};
|
|
9139
|
+
}
|
|
9140
|
+
var SEARCH_SNIPPET_SQL = "snippet(tweets_fts, 1, '<mark>', '</mark>', '...', 16)";
|
|
9141
|
+
function listTimelineItems(query) {
|
|
9142
|
+
const db = getReadDb();
|
|
9143
|
+
const { includeQualityReason = false, lowQualityThreshold, limit = 18 } = query;
|
|
9144
|
+
const normalizedLowQualityThreshold = normalizeLowQualityThreshold(lowQualityThreshold);
|
|
9145
|
+
const ftsSearch = query.search?.trim() ? toFtsSearchQuery(query.search) : "";
|
|
9146
|
+
const plan = buildTimelineItemsQuery(query, ftsSearch ? Number(db.prepare("select count(*) as match_count from tweets_fts where tweets_fts.text match ?").get(ftsSearch).match_count) : 0);
|
|
9147
|
+
let rows = db.prepare(plan.sql).all(...plan.params);
|
|
9148
|
+
if (plan.usedRecentEdgeWindow && rows.length < limit) rows = db.prepare(plan.fallbackSql).all(...plan.fallbackParams);
|
|
9149
|
+
if (plan.ftsSearch && rows.length > 0) {
|
|
9150
|
+
const snippetRows = db.prepare(`select tweet_id, ${SEARCH_SNIPPET_SQL} as search_snippet
|
|
9151
|
+
from tweets_fts
|
|
9152
|
+
where tweets_fts.text match ?
|
|
9153
|
+
and tweet_id in (${rows.map(() => "?").join(",")})`).all(plan.ftsSearch, ...rows.map((row) => String(row.id)));
|
|
9154
|
+
const snippetByTweetId = new Map(snippetRows.map((row) => [String(row.tweet_id), row.search_snippet]));
|
|
9155
|
+
for (const row of rows) row.search_snippet = snippetByTweetId.get(String(row.id));
|
|
9156
|
+
}
|
|
9119
9157
|
const urlExpansionCache = /* @__PURE__ */ new Map();
|
|
9120
9158
|
const profileByHandleCache = /* @__PURE__ */ new Map();
|
|
9121
9159
|
return rows.map((row) => {
|
package/dist/server/server.js
CHANGED
|
@@ -1767,7 +1767,7 @@ var getBaseManifest = getProdBaseManifest;
|
|
|
1767
1767
|
var createEarlyHintsForRequest = createEarlyHintsCollector;
|
|
1768
1768
|
async function loadEntries() {
|
|
1769
1769
|
const [routerEntry, startEntry, pluginAdapters] = await Promise.all([
|
|
1770
|
-
import("./assets/router-
|
|
1770
|
+
import("./assets/router-DVQ3M870.js"),
|
|
1771
1771
|
import("./assets/start-5Z2QO8AU.js"),
|
|
1772
1772
|
import("./assets/empty-plugin-adapters-D9UWiqvJ.js")
|
|
1773
1773
|
]);
|
package/package.json
CHANGED