Sunday, November 5, 2023

Redis Search - Rental Availability

Summary

This post covers a very specific use case of Redis in the short-term rental domain.  Specifically, Redis is used to find property availability in a given geographic area and date/time slot.

Architecture


Code Snippets

Data Load

The code below loads rental properties as Redis JSON objects and US Postal ZIP codes with their associated lat/longs as Redis strings.

async #insertProperties() {
const csvStream = fs.createReadStream("./data/co.csv").pipe(parse({ delimiter: ",", from_line: 2}));
let id = 1;
for await (const row of csvStream) {
const doc = {
"id": id,
"address": {
"coords": `${row[0]} ${row[1]}`,
"number": row[2],
"street": row[3],
"unit": row[4],
"city": row[5],
"state": "CO",
"postcode": row[8]
},
"owner": {
"fname": uniqueNamesGenerator({dictionaries: [names], style: 'capital', length: 1, separator: ' '}),
"lname": uniqueNamesGenerator({dictionaries: [names], style: 'capital', length: 1, separator: ' '}),
},
"type": `${TYPES[Math.floor(Math.random() * TYPES.length)]}`,
"availability": this.#getAvailability(),
"rate": Math.round((Math.random() * 250 + 125) * 100) / 100
}
await this.client.json.set(`property:${id}`, '.', doc);
id++;
if (id > MAX_PROPERTIES) {
break;
}
}
async #insertZips() {
const csvStream = fs.createReadStream("./data/zip_lat_long.csv").pipe(parse({ delimiter: ",", from_line: 2}));
for await (const row of csvStream) {
const zip = row[0];
const lat = row[1];
const lon = row[2];
await this.client.set(`zip:${zip}`, `${lon} ${lat}`);
}
}
view raw rental-load.js hosted with ❤ by GitHub

Property Search

The code below represents an Expressjs route for performing searches on the Redis properties.  The search is performed on rental property type and geographic distance from a given location.

app.post('/property/search', async (req, res) => {
const { type, zip, radius, begin, end } = req.body;
console.log(`app - POST /property/search ${JSON.stringify(req.body)}`);
try {
const loc = await client.get(`zip:${zip}`);
if (!loc) {
throw new Error('Zip code not found');
}
const query = `@type:{${type}} @coords:[${loc} ${radius} mi]`;
const docs = await client.ft.aggregate('propIdx', query,
{
DIALECT: 3,
LOAD: [
'@__key',
{ identifier: `$.availability[?(@.begin<=${begin} && @.end>=${end})]`,
AS: 'match'
}
],
STEPS: [
{ type: AggregateSteps.FILTER,
expression: 'exists(@match)'
},
{
type: AggregateSteps.SORTBY,
BY: {
BY: '@rate',
DIRECTION: 'ASC'
}
},
{
type: AggregateSteps.LIMIT,
from: 0,
size: 3
}
]
});
if (docs && docs.results) {
let properties = [];
for (const result of docs.results) {
const rental_date = JSON.parse(result.match);
const property = {
"key": result.__key,
"rate": result.rate,
"begin": rental_date[0].begin,
"end": rental_date[0].end
};
properties.push(property);
}
console.log(`app - POST /property/search - properties found: ${properties.length}`);
res.status(200).json(properties);
}
else {
console.log('app - POST /property/search - no properties found');
res.status(401).send('No properties found');
}
}
catch (err) {
console.log(`app - POST /property/search - error: ${err.message}`);
res.status(400).json({ 'error': err.message });
}
});

Source


Copyright ©1993-2024 Joey E Whelan, All rights reserved.