Skip to main content

Search & Indexes

ZiWound needs real search โ€” full-text over users/blog posts and geospatial over reports. It gets both directly from MongoDB through the createIndex option on newModel and hand-created indexes. This page shows how, with the framework mechanics in the Aggregation & Filtering docs.

Model-level text indexesโ€‹

Both user and blogPost register a text index at model creation:

// user.ts
export const users = () =>
coreApp.odm.newModel("user", user_pure, user_relations, {
createIndex: { indexSpec: { first_name: "text", last_name: "text", email: "text" } },
});
// blogPost.ts
createIndex: { indexSpec: { title: "text", slug: "text", excerpt: "text" } },

MongoDB then supports a $text query. ZiWound exposes it through acts with a searchTerm in their validator โ€” e.g. getUsers:

// src/user/getUsers/getUsers.val.ts (simplified)
export const getUsersValidator = () =>
object({
set: object({
searchTerm: optional(string()),
limit: optional(number()),
skip: optional(number()),
sort: optional(string()),
}),
get: coreApp.schemas.selectStruct("user", { ... }),
});
// getUsers.fn.ts โ€” where the $text filter is built
export const getUsersFn: ActFn = async (body) => {
const { set } = body.details;
const filter = {};
if (set.searchTerm) {
filter.$text = { $search: set.searchTerm }; // MongoDB text search
}
// + optional sort / pagination
return await users.find({ filter, projection: get, limit, skip, sort });
};

The same pattern powers getRelated / getRelatedPagination on reports and blog posts โ€” the caller passes searchTerm plus pagination and the act builds a $text (or { $regex }) filter.

The report 2dsphere indexโ€‹

Geospatial search needs a 2dsphere index. newModel's createIndex covers text indexes; the report's geo index is created explicitly at boot because it indexes a GeoJSON field:

// mod.ts (after model registration)
await db.collection("report").createIndex({ point: "2dsphere" });

The report's point field is declared as GeoJSON in the pure struct:

point: geoJSONStruct("Point"), // { type: "Point", coordinates: [lng, lat] }

The geospatial act โ€” getRelatedByGeoโ€‹

// src/report/getRelatedByGeo/getRelatedByGeo.fn.ts (simplified)
export const getRelatedByGeoFn: ActFn = async (body) => {
const { set } = body.details;
const { center, radiusKm, status, searchTerm } = set;

const filter = {
point: {
$geoWithin: { $centerSphere: [center, radiusKm / 6371] }, // radians
},
};
if (status) filter.report_status = status;
if (searchTerm) filter.$text = { $search: searchTerm };

return await reports.find({ filter, projection: get, limit, skip });
};

The frontend map (MapLibre GL + Leaflet) sends the map center + radius; the act returns every approved report inside the circle. Because the index is on point, this is a fast geospatial lookup, not a scan.

Pagination & sortingโ€‹

ZiWound's list acts consistently accept limit/skip and a sort string, and return { totalCount, docs } style shapes. The validators use Lesan's optional(number()) + defaulted so the frontend can pass limit: 20, skip: 0 on every request.

Next: Geospatial Queries.