我想将多个 Firestore 查询的结果合并到一个文档数组中,我可以将其绑定到实时更新,就像简单的查询一样。
我正在使用 Vuefire 来简化“使用”Vue 中的数据,所以理想情况下,我想要一种将一组查询发送到 Vuefire 的方法,但它不支持这一点,只支持单个查询、集合或文档。
我已经研究过 Vuefire 在底层所做的事情,即处理
onSnapshot
事件,我想我可能可以编写一个多查询版本,它使用每个查询子集的偏移量拼接到聚合数组中,但它变得很复杂我希望有一个更简单的解决方案。
我为什么要这样做?
通常,在这个特定的应用程序中,我从 Web SDK 查询 Firestore,因此可以轻松地将数据绑定到 UI 并接收更改的即时更新。不过,我现在使用的 Firestore 功能在 Web SDK(矢量搜索查询)中不可用,因此我编写了一个 Firebase 函数,它返回一组文档(或仅返回 ID)。我希望能够在用户与这组文档交互时绑定到它们(就像其他集合一样),因此我编写了一个查询,通过 ID 在 Web SDK 中重新选择这些文档。然而,还有另一个限制,即“where in”子句最多包含 30 个文档,这意味着我只能在一个查询中按 ID 选择 30 个文档。我过去已经解决了这个问题,只需将查询批处理为 30 个文档块,迭代并附加到 doc.data() 的聚合数组。这对于静态数据来说很好,但我想绑定到这些 Firestore 文档 - 理想情况下将手动聚合的查询集的结果发送到 Vuex 的
useCollection
中。
我认为这与解决类似限制的问题类似:使用 Firestore 和 geohashing 监听多个查询的实时更新
我按 id 选择文档列表的“正常”方式如下:
export async function listDocumentsByIds(collectionPath: string, ids: string[]): Promise<object[]> {
const items:object[] = []
const batches = slices(ids, FIRESTORE_ID_QUERY_LIMIT)
for (const batch of batches) {
const q = query(collection(db, collectionPath), where(documentId(), 'in', batch))
await getDocs(q).forEach(qds => items.push({id: qds.id, ...qds.data()}))
}
return items
}
但正如你所看到的,我已经完成了 doc.data(),因此无法订阅文档更改。
所以我想制作一个版本,它返回一个可以订阅的块查询数组:
export async function queryDocumentsByIds(collectionPath: string, ids: string[]): Promise<Query[]> {
const queries:Query[] = []
const batches = slices(ids, FIRESTORE_ID_QUERY_LIMIT)
for (const batch of batches) {
const q = query(collection(db, collectionPath), where(documentId(), 'in', batch))
queries.push(q)
}
return queries
}
理想情况下,我想将
Query[]
发送到 Vuefire 并获得一个 Ref<doc[]>
,并将所有块附加在一起(一厢情愿的想法)。
所以我正在考虑自己将数据附加到包含文档(块)数组的
Ref<doc[]>
中,然后订阅每个查询中的更改,并通过数组索引偏移量查找数组中的文档,就像 i + c * 30
(块大小)。
类似这样的东西(未完成 - 需要找出所有索引偏移量)
export async function useQueryArray(collectionPath: string, ids: string[]){
const multiRef = ref([])
const queries:Query[] = await queryDocumentsByIds(collectionPath, ids)
// listen to all changes on all queries, and merge the changes into our union data ref
for (let i = 0; i < queries.length; i++) {
const q = queries[i]
const offset = i * FIRESTORE_ID_QUERY_LIMIT
onSnapshot(q, (snapshot: QuerySnapshot<DocumentData, DocumentData>) => {
snapshot.docChanges().forEach(change => {
const { newIndex, oldIndex, doc, type } = change
if (type === 'added') {
multiRef.value.splice(newIndex, 0, doc.data())
}
else if (type === 'modified') {
multiRef.value.splice(oldIndex, 1)
multiRef.value.splice(newIndex, 0, doc.data())
}
else if (type === 'removed') {
multiRef.value.splice(oldIndex, 1)
}
})
}, onErrorHandler)
}
}
但在我尝试手动完成此操作之前,我想知道是否有一种更简单的方法来反应性地组合查询“页面” - 考虑到由于 SDK 的限制我只需要拆分页面。
我在“Vue 级别”而不是在 Firestore 或 Vuefire 中解决了这个问题,在意识到这是需要反应式聚合数组的一般情况(即从几个其他数组展平的数组,这些数组都是反应式引用)并且可以是使用
computed
属性完成。
唯一的复杂之处是,在这种情况下,从我的 Firebase 函数返回的向量距离不存在于通过后来的直接 Firestore 查询获取的集合中 - 这需要哈希/查找,并且在附加时还需要在
computed
函数内进行排序块(30 个 ID)。
总而言之,流程是(简化的):
// Call the Function which does the vector search back-end
const similar = await getSimilar(docId, k)
// make lookup table of distances, and list of IDs
similar.forEach(s => {
ids.push(s.id)
distances.push({id:s.id, distance:s.distance})
})
// re-fetch the docs locally to get several VueFire collections (of max 30 IDs)
const queries = await queryDocumentsByIds(collectionPath, ids)
// make all queries into Vuefire reactive collections
chunks.value = queries.map(q => useCollection(q))
// compute finalList - the reactive Firestore collection of all chunks
// add the vector distance back in from the map
const finalList = computed(() => {
return chunks.value.reduce((refs, ref) => {
ref.value.forEach(r => {
const distmap = distances.find(d => d.id === r.id)
refs.push({ ...r, distance: distmap?.distance })
})
return refs
}, [])
.sort((r1, r2) => r1.distance - r2.distance)
})
// I can now use `finalList` as a single array in my Vue paginated table
// and it receives Firestore updates as it if was from a single Vuefire query.
这并不像复制 Vuefire 绑定那么复杂。