SearchService.h header

#include <ew/app/search/SearchService.h>

Namespace ew::app::search

Entry struct

struct ew::app::search::SearchService::Entry

One object as the search sees it: its identity, its title, and the text to match against.

Members

ew::core::foundation::ContentId ew::app::search::SearchService::Entry::id

The object's id, returned with a result so the caller can navigate to it.

QString ew::app::search::SearchService::Entry::title

The object's display title.

QString ew::app::search::SearchService::Entry::body

The text a search matches against: a document's prose (markup removed for a legacy rich-text body) or an entity's field values and aliases.

bool ew::app::search::SearchService::Entry::isDocument = false

True when the object is a document, so a scope filter needs no second dynamic_cast.

bool ew::app::search::SearchService::Entry::isEntity = false

True when the object is an entity, for the same reason.

bool ew::app::search::SearchService::Entry::isBranchScene = false

True when the entry is a branching scene or one of its nodes.

ew::core::foundation::BranchNodeId ew::app::search::SearchService::Entry::nodeId

The node this entry is, for a branch scene's per-node entries; null for the scene's own entry and for every other kind of object.

QString ew::app::search::SearchService::Entry::context

Where the entry sits, for one inside something: "in Toll Bridge".

std::optional<ew::core::branching::BranchNodeType> ew::app::search::SearchService::Entry::nodeType

What kind of node the entry is, for a branch scene's per-node entries.

std::set<QString> ew::app::search::SearchService::Entry::tags

The object's organization tags, lower-cased once here so a tag: term is a lookup rather than a fold of every tag on every object on every keystroke – the same reason the bodies are matched in place rather than copied (F-0085).

SearchResult struct

struct ew::app::search::SearchResult

One hit from a project search: the object found, a display title, a short matching snippet, and a relevance score (higher is more relevant).

Members

ew::core::foundation::ContentId ew::app::search::SearchResult::id

The content object that matched.

QString ew::app::search::SearchResult::title

The object's display title (a document title or an entity name).

QString ew::app::search::SearchResult::snippet

A short excerpt of the body around the first matched term.

int ew::app::search::SearchResult::score = 0

Relevance score; results are returned in descending score order.

ew::core::foundation::BranchNodeId ew::app::search::SearchResult::nodeId

The branching node this hit is inside, or a null id for every other kind of result.

A dialogue hit is a LINE, not a scene. A scene holds hundreds of lines, and a result that could only say which scene a word was in would leave the writer to find it again by eye – which is the same as not finding it.

QString ew::app::search::SearchResult::context

Where the hit sits, for a result that is inside something: "in Toll Bridge". Empty for a document or an entity, whose title already says where they are.

std::optional<ew::core::branching::BranchNodeType> ew::app::search::SearchResult::nodeType

What kind of node the hit is, for a dialogue hit; empty for every other kind of result.

The enum rather than a name, because the words for it belong to whichever client is showing the result – and they have to be translated.

SearchService class

class ew::app::search::SearchService

Full-text search over a project's content. It scans documents (title and body), entities (name and field values) and branching dialogue (scene titles, node text, choice labels, and the names of the variables a node tests or sets) for the query terms, ranks title matches above body matches, and boosts titles that begin with the query. This in-memory scan is the first step; a derived FTS index can back the same interface later for large projects.

Dialogue is indexed a node at a time. Every other content type is one object, one entry; a branch scene is one entry for the scene and one for each of its nodes, so a hit names the line and a caller can select it in the graph. The scene's own entry outranks its nodes' for the scene's name, because the title boost applies to it and not to them.

Members

ew::app::search::SearchService::SearchService(const ew::core::project::Project &project)

Searches project alone (no mounted libraries); not owned.

ew::app::search::SearchService::SearchService(const ew::core::project::Project &project, const app::library::MountedLibraries &libraries)

Searches project and every library it mounts, so results span the whole working graph (shared universes / reference libraries included). Neither is owned.

void ew::app::search::SearchService::rebuild()

Rebuilds the searchable snapshot from the current project state.

The snapshot is built lazily on the first search and then reused, because a search runs once per keystroke while deriving each object's searchable text is not free – reducing a legacy rich-text body to its prose costs an HTML parse, measured at ~25 s across 100 legacy chapters, which per keystroke would be unusable (F-0096). Call this after the project changes; a service that lives only for one interaction never needs to.

Enumerations

enum class SearchScope { Everything, Documents, Entities, BranchScenes }

Which kinds of object a search covers (a search facet).

Functions

QStringList ew::app::search::searchTerms(const QString &query)

The words in query that match text – the query lower-cased and split, with tag: filters removed.

Public because whatever draws a result list has to highlight the words that matched, and it cannot know them without re-deriving them. Splitting the query a second time at the call site would be a second parser to keep in step with this one, and the day they disagreed the highlight would land on the wrong word – or on none, which reads as "no match here".

QString ew::app::search::withTermsMarked(const QString &text, const QStringList &terms)

text as HTML, with every occurrence of a term in terms wrapped in <b> and everything else escaped.

Here rather than in the desktop client because every client shows a result list and every one of them has the same problem: a row is a title, a kind, a place and a hundred characters of excerpt, and without marking the match the reader has to hunt for it. A second implementation in the mobile client would be a second chance to get the escaping wrong.

The escaping is the part that goes wrong. Marking is done by index on the ORIGINAL string and the pieces escaped afterwards – escape first and one ampersand in a title shifts every later index by four, so the bold lands on the wrong characters.