openings 0.1.39 → 0.1.42
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/.codex-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/src/catalog.ts +5 -3
- package/src/index.ts +2 -1
- package/src/text-match.ts +41 -0
- package/src/version.ts +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openings",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.42",
|
|
4
4
|
"description": "Find evidence-grounded jobs, including relevant roles you may not have searched for, without accounts or API keys.",
|
|
5
5
|
"author": { "name": "Openings contributors" },
|
|
6
6
|
"license": "MIT",
|
package/package.json
CHANGED
package/src/catalog.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CASCADE_MINIMUM, CASCADE_WINDOWS, type Company, type Job, type JobAge, type JobSummary, type SearchQuery, type SearchWindow } from "./types.ts";
|
|
2
2
|
import { decodeEntities, providerSpec, type JsonGet } from "./providers.ts";
|
|
3
|
+
import { matchesSearchTerms, searchHaystack, searchTerms } from "./text-match.ts";
|
|
3
4
|
import { crawlSite, sitePostingsToJobs } from "./jobposting-site.ts";
|
|
4
5
|
import { classifyJob, isEligibleForCountry, normalizeLocation } from "./locations.ts";
|
|
5
6
|
import { experienceMatches, normalizeJobExperience } from "./experience.ts";
|
|
@@ -514,10 +515,11 @@ function normalizeRecruitee(company: Company, job: RecruiteeJob): Job {
|
|
|
514
515
|
}
|
|
515
516
|
|
|
516
517
|
function matches(job: Job, query: SearchQuery): boolean {
|
|
517
|
-
const terms = query.query
|
|
518
|
+
const terms = searchTerms(query.query);
|
|
518
519
|
const location = query.location ? normalizeLocation(query.location) : undefined;
|
|
519
|
-
|
|
520
|
-
|
|
520
|
+
// Whole tokens only: a substring match once made "ios" find "Axio Biosolutions".
|
|
521
|
+
const searchable = searchHaystack(`${job.title} ${job.company}`);
|
|
522
|
+
return (terms.length === 0 || matchesSearchTerms(searchable, terms))
|
|
521
523
|
&& (!location || normalizeLocation(job.location).includes(location))
|
|
522
524
|
&& (!query.country || isEligibleForCountry(job, query.country))
|
|
523
525
|
&& (query.remote === undefined || job.remote === query.remote);
|
package/src/index.ts
CHANGED
|
@@ -16,8 +16,9 @@ export const companies: Company[] = Object.entries(companyData).map(([slug, valu
|
|
|
16
16
|
}));
|
|
17
17
|
|
|
18
18
|
export const catalog = createCatalog({ companies });
|
|
19
|
-
export { createCatalog } from "./catalog.ts";
|
|
19
|
+
export { createCatalog, searchJobs } from "./catalog.ts";
|
|
20
20
|
export { EXPERIENCE_VERSION, experienceLabel, experienceMatches, normalizeJobExperience, statedExperience, titleExperience, type Experience } from "./experience.ts";
|
|
21
|
+
export { matchesSearchTerms, searchHaystack, searchTerms, searchTokens } from "./text-match.ts";
|
|
21
22
|
export type { Catalog } from "./catalog.ts";
|
|
22
23
|
export type { Ats, Company, CrawlReport, Job, JobPartition, JobSnapshot, JobSummary, SearchQuery } from "./types.ts";
|
|
23
24
|
export { createCrawlReporter, fetchSeedSnapshot, resolveAggregatorUrl } from "./crawl-reporting.ts";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search matching on whole tokens, so "ios" finds an iOS role and never "Axio Biosolutions".
|
|
3
|
+
* Technology names keep the characters that carry their meaning: c++, c#, node.js, .net.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Everything outside this set separates tokens, so "full-stack" and "full stack" agree. */
|
|
7
|
+
const SEPARATORS = /[^a-z0-9.+#]+/g;
|
|
8
|
+
|
|
9
|
+
/** A trailing plural "s" is dropped on both sides, so "engineers" and "engineer" meet in the middle. */
|
|
10
|
+
function stem(token: string): string {
|
|
11
|
+
if (token.length < 4 || /[+#]/.test(token)) return token;
|
|
12
|
+
return token.endsWith("s") && !token.endsWith("ss") ? token.slice(0, -1) : token;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function searchTokens(text: string): string[] {
|
|
16
|
+
return text.toLocaleLowerCase().replace(SEPARATORS, " ").split(" ")
|
|
17
|
+
.map((token) => token.replace(/^\.+/, "").replace(/\.+$/, ""))
|
|
18
|
+
.filter(Boolean)
|
|
19
|
+
.map(stem);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The text a job is matched against, padded so a term only matches a whole token. A dotted name is also
|
|
23
|
+
* indexed by its parts, so "node" finds "Node.js" while "node.js" still does. */
|
|
24
|
+
export function searchHaystack(text: string): string {
|
|
25
|
+
const indexed = new Set<string>();
|
|
26
|
+
for (const token of searchTokens(text)) {
|
|
27
|
+
indexed.add(token);
|
|
28
|
+
if (token.includes(".")) for (const part of token.split(".")) if (part) indexed.add(stem(part));
|
|
29
|
+
}
|
|
30
|
+
return ` ${[...indexed].join(" ")} `;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The terms of a query; an empty query matches everything. */
|
|
34
|
+
export function searchTerms(query: string | undefined, limit = 8): string[] {
|
|
35
|
+
return searchTokens(query ?? "").slice(0, limit);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Every term must appear as a whole token. */
|
|
39
|
+
export function matchesSearchTerms(haystack: string, terms: string[]): boolean {
|
|
40
|
+
return terms.every((term) => haystack.includes(` ${term} `));
|
|
41
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.1.
|
|
1
|
+
export const VERSION = "0.1.42";
|