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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`); | |
} | |
} |
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.